// ==UserScript==
// @namespace https://github.com/umajho
// @name bangumi-episode-ratings-gadget
// @version 0.2.6
// @description Bangumi 单集评分的超合金组件
// @license MIT
// @website https://github.com/umajho/bangumi-episode-ratings
// @match https://bangumi.tv/*
// @match https://bgm.tv/*
// @match https://chii.in/*
// @grant GM_info
// @grant unsafeWindow
// @grant window.close
// @downloadURL none
// ==/UserScript==
(function() {
"use strict";
const env = {
get APP_ENTRYPOINT() {
return "https://bgm-ep-ratings.deno.dev/";
},
LOCAL_STORAGE_KEY_TOKEN: "bgm_ep_ratings_token",
SEARCH_PARAMS_KEY_TOKEN_COUPON: "bgm_ep_ratings_token_coupon"
};
const version = "0.2.6";
const ENDPOINT_PATHS = {
AUTH: {
BANGUMI_PAGE: "bangumi-page",
CALLBACK: "callback",
REDEEM_TOKEN_COUPON: "redeem-token-coupon"
},
API: {
V0: {
RATE_EPISODE: "rate-episode",
SUBJECT_EPISODES_RATINGS: "subject-episodes-ratings",
EPISODE_RATINGS: "episode-ratings",
MY_EPISODE_RATING: "my-episode-rating"
}
}
};
class Client {
constructor(opts) {
this.subjectEpisodesRatingsCache = {};
this.entrypoint = opts.entrypoint;
this.token = opts.token;
}
get URL_AUTH_BANGUMI_PAGE() {
const url = (
//
new URL(this.buildFullEndpoint("auth", ENDPOINT_PATHS.AUTH.BANGUMI_PAGE))
);
url.searchParams.set("gadget_version", Global.version);
return url.toString();
}
async rateEpisode(opts) {
if (!this.token) return ["auth_required"];
const bodyData = {
claimed_user_id: opts.userID,
subject_id: opts.subjectID,
episode_id: opts.episodeID,
score: opts.score
};
return await this.fetch(
"api/v0",
ENDPOINT_PATHS.API.V0.RATE_EPISODE,
{
method: "POST",
body: JSON.stringify(bodyData)
}
);
}
hasCachedSubjectEpisodesRatings(subjectID) {
return !!this.subjectEpisodesRatingsCache[subjectID];
}
async mustGetSubjectEpisodesRatings(opts) {
if (this.subjectEpisodesRatingsCache[opts.subjectID]) {
return this.subjectEpisodesRatingsCache[opts.subjectID];
}
const searchParams = new URLSearchParams();
if (Global.claimedUserID) {
searchParams.set("claimed_user_id", String(Global.claimedUserID));
searchParams.set("subject_id", String(opts.subjectID));
}
return this.subjectEpisodesRatingsCache[opts.subjectID] = this.fetch(
"api/v0",
ENDPOINT_PATHS.API.V0.SUBJECT_EPISODES_RATINGS,
{ method: "GET", searchParams }
).then(
(resp) => this.subjectEpisodesRatingsCache[opts.subjectID] = unwrap(resp)
);
}
async mustGetEpisodeRatings() {
const searchParams = new URLSearchParams();
if (Global.claimedUserID) {
searchParams.set("claimed_user_id", String(Global.claimedUserID));
searchParams.set("subject_id", String(Global.subjectID));
searchParams.set("episode_id", String(Global.episodeID));
}
const resp = await this.fetch(
"api/v0",
ENDPOINT_PATHS.API.V0.EPISODE_RATINGS,
{
method: "GET",
searchParams
}
);
return unwrap(resp);
}
async getMyEpisodeRating() {
const searchParams = new URLSearchParams();
if (Global.claimedUserID) {
searchParams.set("claimed_user_id", String(Global.claimedUserID));
searchParams.set("subject_id", String(Global.subjectID));
searchParams.set("episode_id", String(Global.episodeID));
}
return await this.fetch(
"api/v0",
ENDPOINT_PATHS.API.V0.MY_EPISODE_RATING,
{
method: "GET",
searchParams
}
);
}
async fetch(group, endpointPath, opts) {
const url = new URL(this.buildFullEndpoint(group, endpointPath));
if (opts.searchParams) {
url.search = opts.searchParams.toString();
}
const headers = new Headers();
if (this.token) {
headers.set("Authorization", `Basic ${this.token}`);
}
headers.set("X-Gadget-Version", Global.version);
try {
const resp = await fetch(url, {
method: opts.method,
headers,
body: opts.body
});
const respJSON = await resp.json();
if (respJSON[0] === "error" && respJSON[1] === "AUTH_REQUIRED") {
if (Global.token.getValueOnce() !== null) {
Global.token.setValue(null);
}
return ["auth_required"];
}
return respJSON;
} catch (e) {
const operation = `fetch \`${opts.method} ${url}\``;
console.error(`${operation} \u5931\u8D25`, e);
return ["error", "UNKNOWN", `${operation} \u5931\u8D25\uFF1A ${e}`];
}
}
buildFullEndpoint(group, endpointPath) {
return join(join(this.entrypoint, group + "/"), endpointPath);
}
async mustRedeemTokenCoupon(tokenCoupon) {
const data = await this.fetch(
"auth",
ENDPOINT_PATHS.AUTH.REDEEM_TOKEN_COUPON,
{
method: "POST",
body: JSON.stringify({ tokenCoupon })
}
);
return unwrap(data);
}
clearCache() {
this.subjectEpisodesRatingsCache = {};
}
}
function join(base, url) {
return new URL(url, base).href;
}
function unwrap(resp) {
if (!Array.isArray(resp) || resp[0] !== "ok" && resp[0] !== "error") {
console.error("Unsupported response", resp);
throw new Error(`Unsupported response: ${JSON.stringify(resp)}`);
}
if (resp[0] === "error") throw new Error(resp[2]);
return resp[1];
}
class Watched {
constructor(_value) {
this._value = _value;
this._watchers = [];
}
getValueOnce() {
return this._value;
}
setValue(newValue) {
const oldValue = this._value;
this._value = newValue;
this._watchers.forEach((w) => w(newValue, oldValue));
}
watchDeferred(cb) {
this._watchers.push(cb);
return () => {
this._watchers = this._watchers.filter((w) => w !== cb);
};
}
watch(cb) {
cb(this._value, void 0);
return this.watchDeferred(cb);
}
}
const global = {};
const Global = global;
function initializeGlobal() {
Object.assign(global, makeGlobal());
}
function makeGlobal() {
const { subjectID, episodeID } = (() => {
let subjectID2 = null;
let episodeID2 = null;
const pathParts = window.location.pathname.split("/").filter(Boolean);
if (pathParts[0] === "subject") {
subjectID2 = Number(pathParts[1]);
} else if (pathParts.length === 2 && pathParts[0] === "ep") {
episodeID2 = Number(pathParts[1]);
const subjectHref = $("#headerSubject > .nameSingle > a").attr("href");
subjectID2 = Number(subjectHref.split("/")[2]);
}
return { subjectID: subjectID2, episodeID: episodeID2 };
})();
const claimedUserID = (() => {
if ("unsafeWindow" in window) {
return window.unsafeWindow.CHOBITS_UID || null;
}
return window.CHOBITS_UID || null;
})();
if (claimedUserID === null) {
localStorage.removeItem(env.LOCAL_STORAGE_KEY_TOKEN);
}
const token = new Watched(
localStorage.getItem(env.LOCAL_STORAGE_KEY_TOKEN)
);
window.addEventListener("storage", (ev) => {
if (ev.key !== env.LOCAL_STORAGE_KEY_TOKEN) return;
if (ev.newValue === token.getValueOnce()) return;
token.setValue(ev.newValue);
});
const client = new Client({
entrypoint: env.APP_ENTRYPOINT,
token: token.getValueOnce()
});
token.watchDeferred((newToken) => {
if (newToken) {
localStorage.setItem(env.LOCAL_STORAGE_KEY_TOKEN, newToken);
} else {
localStorage.removeItem(env.LOCAL_STORAGE_KEY_TOKEN);
}
client.token = newToken;
client.clearCache();
});
return {
version,
subjectID,
episodeID,
claimedUserID,
token,
client
};
}
const scores = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
function describeScore(score) {
return [
[9.5, "\u8D85\u795E\u4F5C"],
[8.5, "\u795E\u4F5C"],
[7.5, "\u529B\u8350"],
[6.5, "\u63A8\u8350"],
[5.5, "\u8FD8\u884C"],
[4.5, "\u4E0D\u8FC7\u4E0D\u5931"],
[3.5, "\u8F83\u5DEE"],
[2.5, "\u5DEE"],
[1.5, "\u5F88\u5DEE"]
].find(([min, _]) => score >= min)?.[1] ?? "\u4E0D\u5FCD\u76F4\u89C6";
}
function describeScoreEx(score) {
let description = `${describeScore(score)} ${score}`;
if (score === 1 || score === 10) {
description += " (\u8BF7\u8C28\u614E\u8BC4\u4EF7)";
}
return description;
}
function renderStars(el, props) {
el = $(
/*html*/
`
`
).replaceAll(el);
for (const score of scores) {
const starEl = $(
/*html*/
`
`
);
el.append(starEl);
const aEl = starEl.find("a");
aEl.text(score);
aEl.attr("title", describeScoreEx(score));
starEl.on("mouseover", () => props.hoveredScore.setValue(score)).on("mouseout", () => props.hoveredScore.setValue(null)).on("click", () => props.onRateEpisode(score));
}
function updateStarsContainer(params) {
if (params[0] === "invisible") {
el.css("display", "none");
return;
}
el.css("display", "");
const [_, { ratedScore, hoveredScore }] = params;
const isHovering = hoveredScore !== null;
const maxScoreToHighlight = hoveredScore ?? ratedScore ?? null;
{
let alarmScore = maxScoreToHighlight;
if (alarmScore === "cancel") {
alarmScore = ratedScore;
}
props.onUpdateScoreToAlarm(alarmScore);
}
const starEls = el.find(".star-rating");
for (const score of scores) {
const starEl = starEls.eq(score - 1);
starEl.removeClass("star-rating-on").removeClass("star-rating-hover");
if (typeof maxScoreToHighlight === "number" && score <= maxScoreToHighlight) {
starEl.addClass(isHovering ? "star-rating-hover" : "star-rating-on");
}
}
$(el).find(".rating-cancel").removeClass("star-rating-hover");
if (hoveredScore === "cancel") {
$(el).find(".rating-cancel").addClass("star-rating-hover");
}
}
return { updateStarsContainer };
}
function renderMyRating(el, props) {
const ratedScore = new Watched(props.ratedScore);
const hoveredScore = new Watched(null);
el = $(
/*html*/
`
\u6211\u7684\u8BC4\u4EF7:
`
).replaceAll(el);
const starsContainerEl = el.find(".stars-container");
const { updateStarsContainer } = renderStars(starsContainerEl, {
hoveredScore,
onRateEpisode: rateEpisode,
onUpdateScoreToAlarm: (score) => {
if (score !== null) {
$(el).find(".alarm").text(describeScoreEx(score));
} else {
$(el).find(".alarm").text("");
}
}
});
$(el).find(".rating-cancel").on("mouseover", () => hoveredScore.setValue("cancel")).on("mouseout", () => hoveredScore.setValue(null)).on("click", () => rateEpisode(null));
ratedScore.watchDeferred(
(ratedScore2) => updateStarsContainer(["normal", {
ratedScore: ratedScore2,
hoveredScore: hoveredScore.getValueOnce()
}])
);
hoveredScore.watch((hoveredScore2) => {
updateStarsContainer(["normal", {
ratedScore: ratedScore.getValueOnce(),
hoveredScore: hoveredScore2
}]);
});
const messageEl = el.find(".message");
function updateMessage(value) {
messageEl.attr("style", "");
switch (value[0]) {
case "none": {
messageEl.text("");
messageEl.css("display", "none");
break;
}
case "processing": {
messageEl.text("\u5904\u7406\u4E2D\u2026");
messageEl.css("color", "gray");
break;
}
case "loading": {
messageEl.text("\u52A0\u8F7D\u4E2D\u2026");
messageEl.css("color", "gray");
break;
}
case "error": {
messageEl.text(value[1]);
messageEl.css("color", "red");
break;
}
case "auth_link": {
messageEl.html(
/*html*/
`
\u82E5\u8981\u67E5\u770B\u6216\u63D0\u4EA4\u81EA\u5DF1\u7684\u5355\u96C6\u8BC4\u5206\uFF0C
\u8BF7\u5148\u6388\u6743\u6B64\u5E94\u7528\u3002
\u5355\u96C6\u8BC4\u5206\u5E94\u7528\u9700\u8981\u4EE5\u6B64\u6765\u786E\u8BA4\u767B\u5F55\u8005\u3002
`
);
$(messageEl).find("a").attr(
"href",
Global.client.URL_AUTH_BANGUMI_PAGE
);
break;
}
case "requiring_fetch": {
if (props.canRefetchAfterAuth) {
messageEl.html(
/*html*/
`
\u70B9\u51FB\u6216\u5237\u65B0\u672C\u9875\u4EE5\u83B7\u53D6\u3002
`
);
$(messageEl).find("button").on("click", async () => {
updateMessage(["loading"]);
const resp = await Global.client.getMyEpisodeRating();
if (resp[0] === "auth_required") {
Global.token.setValue(null);
} else if (resp[0] === "error") {
const [_, _name, message] = resp;
updateMessage(["error", message]);
} else if (resp[0] === "ok") {
const [_, data] = resp;
updateMessage(["none"]);
ratedScore.setValue(data.score);
} else ;
});
} else {
messageEl.text("\u8BF7\u5237\u65B0\u672C\u9875\u4EE5\u83B7\u53D6\u3002 ");
}
break;
}
}
}
updateMessage(["none"]);
Global.token.watch((newToken, oldToken) => {
if (newToken) {
if (oldToken !== void 0) {
if (props.isPrimary) {
updateMessage(["requiring_fetch"]);
}
updateStarsContainer(["invisible"]);
} else {
updateMessage(["none"]);
}
} else {
if (props.isPrimary) {
updateMessage(["auth_link"]);
} else {
el.css("display", "none");
}
updateStarsContainer(["invisible"]);
}
});
async function rateEpisode(scoreToRate) {
if (!Global.token.getValueOnce()) return;
updateMessage(["processing"]);
const resp = await Global.client.rateEpisode({
userID: Global.claimedUserID,
subjectID: Global.subjectID,
episodeID: props.episodeID,
score: scoreToRate
});
if (resp[0] === "auth_required") {
updateMessage(["auth_link"]);
} else if (resp[0] === "error") {
const [_, _name, message] = resp;
updateMessage(["error", message]);
} else if (resp[0] === "ok") {
const [_, data] = resp;
updateMessage(["none"]);
ratedScore.setValue(data.score);
} else ;
}
}
function renderScoreboard(el, props) {
el = $(
/*html*/
`
`
).replaceAll(el);
function updateNumber(score) {
if (Number.isNaN(score)) {
$(el).find(".number").text(0 .toFixed(1));
$(el).find(".description").text("--");
} else {
$(el).find(".number").text(score.toFixed(4));
$(el).find(".description").text(describeScore(score));
}
}
updateNumber(props.score);
}
function renderScoreChart(el, props) {
const { votesData } = props;
el = $(
/*html*/
`
`
).replaceAll(el);
$(el).find(".votes").text(votesData.totalVotes);
const chartEl = el.find(".horizontalChart");
const totalVotes = votesData.totalVotes;
const votesOfMostVotedScore = votesData.votesOfMostVotedScore;
for (const score of scores) {
const votes = votesData.getScoreVotes(score);
const barEl = $("");
chartEl.prepend(barEl);
renderBar(barEl, {
score,
votes,
totalVotes,
votesOfMostVotedScore,
updateTooltip
});
}
function updateTooltip(props2) {
let tooltipEl = $(chartEl).find(".tooltip");
if (props2.score === null) {
tooltipEl.css("display", "none");
return;
}
tooltipEl.css("display", "block");
const barEl = $(chartEl).find(`li`).eq(10 - props2.score);
const barElRelativeOffsetLeft = barEl.offset().left - el.offset().left;
tooltipEl.css("left", `${barElRelativeOffsetLeft + barEl.width() / 2}px`);
let scoreVotes = votesData.getScoreVotes(props2.score);
const percentage = votesData.totalVotes ? scoreVotes / votesData.totalVotes * 100 : 0;
$(tooltipEl).find(".tooltip-inner").text(
`${percentage.toFixed(2)}% (${scoreVotes}\u4EBA)`
);
}
updateTooltip({ score: null });
return el;
}
function renderBar(el, props) {
el = $(
/*html*/
`
`
).replaceAll(el);
const percentage = (props.votes / props.totalVotes * 100).toFixed(2);
$(el).find(".textTip").attr(
"data-original-title",
`${percentage}% (${props.votes}\u4EBA)`
);
$(el).find(".label").text(props.score);
const height = (props.votes / props.votesOfMostVotedScore * 100).toFixed(2);
$(el).find(".count").css("height", `${height}%`);
$(el).find(".count").text(`(${props.votes})`);
$(el).on("mouseover", () => props.updateTooltip({ score: props.score })).on("mouseout", () => props.updateTooltip({ score: null }));
}
class VotesData {
constructor(data) {
this.data = data;
this.totalVotesCache = null;
this.averageScoreCache = null;
this.mostVotedScoreCache = null;
}
getScoreVotes(score) {
return this.data[score] ?? 0;
}
get totalVotes() {
if (this.totalVotesCache) return this.totalVotesCache;
let totalVotes = 0;
for (const score of scores) {
totalVotes += this.getScoreVotes(score);
}
return this.totalVotesCache = totalVotes;
}
get averageScore() {
if (this.averageScoreCache) return this.averageScoreCache;
let totalScore = 0;
for (const score of scores) {
totalScore += this.getScoreVotes(score) * score;
}
return this.averageScoreCache = totalScore / this.totalVotes;
}
get mostVotedScore() {
if (this.mostVotedScoreCache) return this.mostVotedScoreCache;
let mostVotedScore = scores[0];
for (const score of scores.slice(1)) {
if (this.getScoreVotes(score) > this.getScoreVotes(mostVotedScore)) {
mostVotedScore = score;
}
}
return this.mostVotedScoreCache = mostVotedScore;
}
get votesOfMostVotedScore() {
return this.getScoreVotes(this.mostVotedScore);
}
}
async function processEpPage() {
const scoreboardEl = $(
/*html*/
`
\u5355\u96C6\u8BC4\u5206\u52A0\u8F7D\u4E2D\u2026
`
);
$("#columnEpA").prepend(scoreboardEl);
const ratingsData = await Global.client.mustGetEpisodeRatings();
const votesData = new VotesData(
ratingsData.votes
);
renderScoreboard(scoreboardEl, { score: votesData.averageScore });
const scoreChartEl = $("").insertBefore("#columnEpA > .epDesc");
renderScoreChart(scoreChartEl, { votesData });
$(
/*html*/
``
).insertAfter("#columnEpA > .epDesc");
const myRatingEl = $("").insertAfter(".singleCommentList > .board");
if (!ratingsData.my_rating) {
Global.token.setValue(null);
}
renderMyRating(myRatingEl, {
episodeID: Global.episodeID,
ratedScore: ratingsData.my_rating?.score ?? null,
isPrimary: true,
canRefetchAfterAuth: true
});
}
function renderRateInfo(el, props) {
el = $(
/*html*/
`
`
).replaceAll(el);
const rateInfoEl = el.find(".rateInfo");
const buttonEl = el.find("button");
props.votesData.watch((votesData) => {
if (!votesData) {
el.css("display", "none");
return;
}
el.css("display", "");
const score = votesData.averageScore;
if (Number.isNaN(score)) {
$(rateInfoEl).find(".maybe-starlight").removeClass().addClass("maybe-starlight");
$(rateInfoEl).find("small.fade").text("--");
} else {
$(rateInfoEl).find(".maybe-starlight").addClass("starlight").addClass(`stars${Math.round(score)}`);
$(rateInfoEl).find("small.fade").text(score.toFixed(4));
}
$(rateInfoEl).find(".tip_j").text(`(${votesData.totalVotes}\u4EBA\u8BC4\u5206)`);
});
buttonEl.on("click", () => {
console.log("111");
rateInfoEl.css("display", "");
buttonEl.css("display", "none");
props.onReveal?.();
});
props.requiresClickToReveal.watch((requiresClickToReveal) => {
if (requiresClickToReveal) {
rateInfoEl.css("display", "none");
buttonEl.css("display", "");
} else {
rateInfoEl.css("display", "");
buttonEl.css("display", "none");
}
});
}
let isNavigatingAway = false;
$(window).on("beforeunload", () => {
isNavigatingAway = true;
});
function processCluetip() {
const el = $("#cluetip");
let counter = 0;
const revealed = {};
async function update(opts) {
const popupEl = $(el).find(".prg_popup");
if (popupEl.attr("data-bgm-ep-ratings-initialized")) return;
popupEl.attr("data-bgm-ep-ratings-initialized", "true");
counter++;
const currentCounter = counter;
if (!Global.client.hasCachedSubjectEpisodesRatings(opts.subjectID)) {
await new Promise((resolve) => setTimeout(resolve, 250));
if (isNavigatingAway || currentCounter !== counter || !popupEl.is(":visible")) {
return;
}
}
const loadingEl = $(
/*html*/
`
\u5355\u96C6\u8BC4\u5206\u52A0\u8F7D\u4E2D\u2026
`
).insertBefore($(popupEl).find(".tip .board"));
const epsRatings = await Global.client.mustGetSubjectEpisodesRatings({
subjectID: opts.subjectID
});
loadingEl.remove();
if (currentCounter !== counter) return;
const votesData = new Watched(
new VotesData(
epsRatings.episodes_votes[opts.episodeID] ?? {}
)
);
const requiresClickToReveal = new Watched(false);
requiresClickToReveal.setValue(
!(opts.hasUserWatched || revealed[`${opts.subjectID}:${opts.episodeID}`] || !votesData.getValueOnce().totalVotes)
);
function revealScore() {
revealed[`${opts.subjectID}:${opts.episodeID}`] = true;
requiresClickToReveal.setValue(false);
}
const rateInfoEl = $("").insertBefore($(popupEl).find(".tip .board"));
renderRateInfo(rateInfoEl, {
votesData,
requiresClickToReveal,
onReveal: () => {
revealed[`${opts.subjectID}:${opts.episodeID}`] = true;
}
});
$(popupEl).find(".epStatusTool > a.ep_status").each((_, epStatusEl) => {
if (epStatusEl.id.startsWith("Watched")) {
$(epStatusEl).on("click", () => revealScore());
}
});
}
return { update };
}
async function processRootPage() {
const { update: updateCluetip } = processCluetip();
let isMouseOver = false;
$("ul.prg_list > li").each((_, liEl) => {
if (!$(liEl).find(".load-epinfo").length) return;
$(liEl).on("mouseover", () => {
if (isMouseOver) return;
isMouseOver = true;
const aEl = $(liEl).find("a");
const subjectID = Number($(aEl).attr("subject_id"));
const episodeID = (() => {
const href = $(aEl).attr("href");
const match = href.match(/^\/ep\/(\d+)/);
return Number(match[1]);
})();
updateCluetip({
subjectID,
episodeID,
hasUserWatched: aEl.hasClass("epBtnWatched")
});
}).on("mouseout", () => {
isMouseOver = false;
});
});
}
async function processSubjectPage() {
const { update: updateCluetip } = processCluetip();
let isMouseOver = false;
$("ul.prg_list > li").each((_, liEl) => {
$(liEl).on("mouseover", () => {
if (isMouseOver) return;
isMouseOver = true;
const aEl = $(liEl).find("a");
const episodeID = (() => {
const href = $(aEl).attr("href");
const match = href.match(/^\/ep\/(\d+)/);
return Number(match[1]);
})();
updateCluetip({
subjectID: Global.subjectID,
episodeID,
hasUserWatched: aEl.hasClass("epBtnWatched")
});
}).on("mouseout", () => {
isMouseOver = false;
});
});
}
async function processSubjectEpListPage() {
const editEpBatchEl = $('[name="edit_ep_batch"]');
let loadingEl;
$(editEpBatchEl).find("li").each((_, li) => {
if (!$(li).find('[name="ep_mod[]"]').length) return;
$(
/*html*/
``
).insertAfter($(li).find("h6"));
loadingEl = $(
/*html*/
`
\u5355\u96C6\u8BC4\u5206\u52A0\u8F7D\u4E2D\u2026
`
);
$(li).append(loadingEl);
return false;
});
const epsRatings = await Global.client.mustGetSubjectEpisodesRatings({
subjectID: Global.subjectID
});
if (loadingEl) {
loadingEl.remove();
}
if (!epsRatings.my_ratings) {
Global.token.setValue(null);
}
let isFirst_ = true;
$(editEpBatchEl).find("li").each((_, li) => {
if (!$(li).find('[name="ep_mod[]"]').length) return;
const isFirst = isFirst_;
isFirst_ = false;
if (!isFirst) {
$(
/*html*/
``
).insertAfter($(li).find("h6"));
}
const episodeID = (() => {
const href = $(li).find("> h6 > a").attr("href");
const match = /\/ep\/(\d+)/.exec(href);
return Number(match[1]);
})();
const ratings = epsRatings.episodes_votes[episodeID];
if (!epsRatings.is_certain_that_episodes_votes_are_integral && ratings === void 0) {
return;
}
const votesData = new VotesData(ratings ?? {});
const myRating = epsRatings.my_ratings?.[episodeID];
const hasUserWatched = $(li).find(".statusWatched").length || // 在 “看过” 之类不能修改章节观看状态的情况下,没法确认用户是否看过,但至
// 少可以假设用户在给了某集评分的时候是看过那一集的。
myRating !== void 0;
const myRatingEl = $("");
$(li).append(myRatingEl);
renderMyRating(myRatingEl, {
episodeID,
ratedScore: myRating ?? null,
isPrimary: isFirst,
canRefetchAfterAuth: false
});
const rateInfoEl = $("");
$(li).append(rateInfoEl);
renderRateInfo(rateInfoEl, {
votesData: new Watched(votesData),
requiresClickToReveal: (
//
new Watched(!hasUserWatched && !!votesData.totalVotes)
)
});
$(li).append($(
/*html*/
``
));
});
}
function migrate() {
const tokenInWrongPlace = localStorage.getItem("bgm_test_app_token");
if (tokenInWrongPlace) {
localStorage.setItem(env.LOCAL_STORAGE_KEY_TOKEN, tokenInWrongPlace);
localStorage.removeItem("bgm_test_app_token");
}
const searchParams = new URLSearchParams(window.location.search);
const tokenCouponInWrongPlace = searchParams.get("bgm_test_app_token_coupon");
if (tokenCouponInWrongPlace) {
searchParams.set(
env.SEARCH_PARAMS_KEY_TOKEN_COUPON,
tokenCouponInWrongPlace
);
searchParams.delete("bgm_test_app_token_coupon");
let newURL = `${window.location.pathname}?${searchParams.toString()}`;
window.history.replaceState(null, "", newURL);
}
}
async function main() {
const isInUserScriptRuntime = typeof GM_info !== "undefined";
if ($('meta[name="__bgm_ep_ratings__initialized"]').length) {
console.warn(
"\u68C0\u6D4B\u5230\u672C\u811A\u672C/\u8D85\u5408\u91D1\u7EC4\u4EF6\uFF08\u5355\u96C6\u8BC4\u5206 by Umajho A.K.A. um\uFF09\u5148\u524D\u5DF2\u7ECF\u521D\u59CB\u5316\u8FC7\uFF0C\u672C\u5B9E\u4F8B\u5C06\u4E0D\u4F1A\u7EE7\u7EED\u8FD0\u884C\u3002",
{ version: Global.version, isInUserScriptRuntime }
);
return;
}
$('').appendTo("head");
const searchParams = new URLSearchParams(window.location.search);
const tokenCoupon = searchParams.get(env.SEARCH_PARAMS_KEY_TOKEN_COUPON);
if (tokenCoupon) {
searchParams.delete(env.SEARCH_PARAMS_KEY_TOKEN_COUPON);
let newURL = `${window.location.pathname}`;
if (searchParams.size) {
newURL += `?${searchParams.toString()}`;
}
window.history.replaceState(null, "", newURL);
Global.token.setValue(
await Global.client.mustRedeemTokenCoupon(tokenCoupon)
);
window.close();
}
const pathParts = window.location.pathname.split("/").filter(Boolean);
if (!pathParts.length) {
await processRootPage();
} else if (pathParts.length === 2 && pathParts[0] === "subject") {
await processSubjectPage();
} else if (pathParts.length === 3 && pathParts[0] === "subject" && pathParts[2] === "ep") {
await processSubjectEpListPage();
} else if (pathParts.length === 2 && pathParts[0] === "ep") {
await processEpPage();
}
}
migrate();
initializeGlobal();
main();
})();