Eh帳號管理 - 自動登入與多帳號切換,支持共享帳號與導入導出cookie,修改自greasyfork.org/scripts/470710
// ==UserScript==
// @name [Eh] Account Manager
// @name:zh-CN [Eh] 账号管理
// @name:zh-TW [Eh] 帳號管理
// @name:ja [Eh] アカウント管理
// @name:ko [Eh] 계정 관리
// @name:ru [Eh] Управление аккаунтами
// @name:en [Eh] Account Manager
// @version 1.0.1
// @author Li
// @description Eh Account Manager - Auto login and switch between multiple accounts, supports shared accounts and import/export cookie, modified from greasyfork.org/scripts/470710
// @description:zh-TW Eh帳號管理 - 自動登入與多帳號切換,支持共享帳號與導入導出cookie,修改自greasyfork.org/scripts/470710
// @description:zh-CN Eh账号管理 - 自动登入与多账号切换,支持共享账号与导入导出cookie,修改自greasyfork.org/scripts/470710
// @description:ja Eh アカウント管理 - 自動ログインと複数アカウント切替、共有アカウントとcookieのインポート/エクスポート対応、greasyfork.org/scripts/470710 から改変
// @description:ko Eh 계정 관리 - 자동 로그인 및 다중 계정 전환, 공유 계정 및 cookie 가져오기/내보내기 지원, greasyfork.org/scripts/470710 에서 수정
// @description:ru Eh Управление аккаунтами - Автоматический вход и переключение между аккаунтами, поддерживает общие аккаунты и импорт/экспорт cookie, изменено из greasyfork.org/scripts/470710
// @description:en Eh Account Manager - Auto login and switch between multiple accounts, supports shared accounts and import/export cookie, modified from greasyfork.org/scripts/470710
// @noframes
// @connect *
// @match *://e-hentai.org/*
// @match *://exhentai.org/*
// @icon https://e-hentai.org/favicon.ico
// @license MPL-2.0
// @namespace https://greasyfork.org/users/1069880
// @require https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.19.0/js/md5.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_addValueChangeListener
// @run-at document-start
// ==/UserScript==
// thanks:
// https://greasyfork.org/scripts/470710 (MPL-2.0)(greasyfork.org/users/989635-canaan-hs)
(async () => {
const MenuIds = new Set();
const ListenKeys = new Set();
const LangMap = {
ko: "Korea", "ko-KR": "Korea",
ja: "Japan", "ja-JP": "Japan",
ru: "Russia", "ru-RU": "Russia",
en: "English", "en-US": "English", "en-GB": "English", "en-AU": "English", "en-CA": "English", "en-NZ": "English", "en-IE": "English", "en-ZA": "English", "en-IN": "English",
zh: "Simplified", "zh-CN": "Simplified", "zh-SG": "Simplified", "zh-MY": "Simplified",
"zh-TW": "Traditional", "zh-HK": "Traditional", "zh-MO": "Traditional"
};
const Lib = {
$domain: location.hostname,
$q: s => document.querySelector(s),
cookie: v => v == null ? document.cookie : document.cookie = v,
getV: (k, d) => GM_getValue(k, d) ?? null,
setV: (k, v) => GM_setValue(k, v),
getJV: (k, d) => { try { return JSON.parse(GM_getValue(k, d)); } catch { return d; } },
setJV: (k, v) => GM_setValue(k, JSON.stringify(v)),
getDate: (fmt = "{year}-{month}-{date} {hour}:{minute}:{second}") => {
const now = new Date();
const map = {
year: now.getFullYear(),
month: `${now.getMonth() + 1}`.padStart(2, "0"),
date: `${now.getDate()}`.padStart(2, "0"),
hour: `${now.getHours()}`.padStart(2, "0"),
minute: `${now.getMinutes()}`.padStart(2, "0"),
second: `${now.getSeconds()}`.padStart(2, "0")
};
return fmt.replace(/{([^}]+)}/g, (_, k) => map[k] ?? "Error");
},
local: (key, opts = {}) => {
if (opts.value !== undefined) {
localStorage.setItem(key, JSON.stringify(opts.value));
return true;
}
const raw = localStorage.getItem(key);
if (raw == null) return opts.error ?? null;
try { return JSON.parse(raw); } catch { return opts.error ?? null; }
},
addStyle: (css, id) => {
const style = document.createElement("style");
style.id = id;
style.textContent = css;
const mount = () => (document.head || document.documentElement).appendChild(style);
if (document.head || document.documentElement) mount();
else window.addEventListener("DOMContentLoaded", mount, { once: true });
},
translMatcher: (map, lang = navigator.language) => map[LangMap[lang]] ?? map[LangMap["en-US"]],
regMenu: (items, { name = "Menu" } = {}) => {
let index = 1;
for (const [label, cfg] of Object.entries(items)) {
const id = `${name}-${index++}`;
const opts = typeof cfg === "function" ? { func: cfg } : cfg;
GM_registerMenuCommand(label, () => opts.func(), { id, title: opts.desc, autoClose: opts.close, accessKey: opts.hotkey });
MenuIds.add(id);
}
},
unMenu: id => {
if (MenuIds.has(id)) {
GM_unregisterMenuCommand(id);
MenuIds.delete(id);
}
},
storeListen: (keys, cb) => {
keys.forEach(key => {
if (!ListenKeys.has(key)) {
ListenKeys.add(key);
GM_addValueChangeListener(key, (k, ov, nv, far) => cb({ key: k, ov, nv, far }));
}
});
}
};
const domain = Lib.$domain;
const {
Transl
} = Language();
(async function ImportStyle() {
let show_style, button_style, button_hover, toast_style, acc_style;
if (domain === "e-hentai.org") {
button_hover = "color: #8f4701;";
toast_style = "background-color: #5C0D12; color: #fefefe;";
show_style = "background-color: #fefefe; border: 3px ridge #34353b;";
acc_style = "color: #5C0D12; background-color: #fefefe; border: 2px solid #B5A4A4;";
button_style = "color: #5C0D12; border: 2px solid #B5A4A4; background-color: #fefefe;";
} else if (domain === "exhentai.org") {
button_hover = "color: #989898;";
toast_style = "background-color: #fefefe; color: #5C0D12;";
show_style = "background-color: #34353b; border: 2px ridge #5C0D12;";
acc_style = "color: #f1f1f1; background-color: #34353b; border: 2px solid #8d8d8d;";
button_style = "color: #fefefe; border: 2px solid #8d8d8d; background-color: #34353b;";
Lib.addStyle(`
body {
padding: 2px;
color: #f1f1f1;
text-align: center;
background: #34353b;
}
`);
}
Lib.addStyle(`
.toast-container {
top: 1.5rem;
right: 1.5rem;
z-index: 9999;
position: fixed;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.5rem;
pointer-events: none;
}
.toast {
${toast_style}
padding: 0.5rem 1rem;
font-size: 1rem;
white-space: nowrap;
border-radius: 2px;
box-shadow: 0 2px 6px rgba(0,0,0,0.3);
opacity: 0;
transform: translateX(1rem);
transition: opacity 0.25s ease, transform 0.25s ease;
pointer-events: auto;
}
.toast.show {
opacity: 1;
transform: translateX(0);
}
.modal-background {
top: 50%;
left: 50%;
opacity: 0;
width: 100%;
height: 100%;
z-index: 8888;
overflow: auto;
position: fixed;
transition: opacity 0.25s ease;
background-color: rgba(0,0,0,0);
transform: translate(-50%, -50%);
}
.acc-modal {
${show_style}
width: 18%;
overflow: auto;
margin: 11rem auto;
border-radius: 4px;
}
.acc-select-flex {
display: flex;
align-items: center;
flex-direction: initial;
justify-content: space-around;
}
.acc-button-flex {
display: flex;
padding: 0 0 15px 0;
justify-content: center;
}
.acc-select {
${acc_style}
padding: 4px;
min-width: 10rem;
margin: 1.1rem 1.4rem 1.5rem 1.4rem;
font-weight: bold;
cursor: pointer;
font-size: 1.2rem;
text-align: center;
border-radius: 2px;
}
.show-modal {
${show_style}
width: 25%;
padding: 1.5rem;
overflow: auto;
margin: 5rem auto;
text-align: left;
border-radius: 4px;
border-collapse: collapse;
}
.modal-button {
${button_style}
top: 0;
margin: 3% 2%;
font-size: 14px;
font-weight: bold;
border-radius: 2px;
}
.modal-button:hover, .modal-button:focus {
${button_hover}
cursor: pointer;
text-decoration: none;
}
.set-modal {
${show_style}
width: 35%;
padding: 0.3rem;
overflow: auto;
text-align: center;
border-radius: 4px;
border-collapse: collapse;
margin: 2% auto 8px auto;
}
.set-box {
display: flex;
margin: 0.6rem;
font-weight: bold;
flex-direction: column;
align-items: flex-start;
}
.set-list {
width: 95%;
font-weight: 550;
font-size: 1.1rem;
text-align: center;
}
hr {
width: 98%;
opacity: 0.2;
border: 1px solid;
margin-top: 1.3rem;
}
label {
margin: 0.4rem;
font-size: 0.9rem;
}
.current-login {
text-align: center;
font-size: 0.95rem;
opacity: 0.75;
margin: 0.2rem 0 0.9rem 0;
padding: 0.35rem;
border-radius: 2px;
}
.account-list {
margin: 0.6rem;
text-align: left;
}
.account-item {
display: flex;
align-items: center;
padding: 0.6rem 0.5rem;
gap: 0.6rem;
flex-wrap: wrap;
border-bottom: 1px solid rgba(128,128,128,0.15);
}
.account-item:last-child {
border-bottom: none;
}
.account-note {
flex: 1;
min-width: 100px;
padding: 4px 8px;
font-size: 0.95rem;
}
.account-action {
cursor: pointer;
font-size: 0.9rem;
padding: 4px 10px;
white-space: nowrap;
border: 1px solid;
background: transparent;
color: inherit;
}
.account-action:hover {
opacity: 0.7;
}
.account-action:disabled {
opacity: 0.35;
cursor: default;
}
.account-index {
font-weight: bold;
font-size: 1rem;
min-width: 2rem;
}
.modal-close {
position: absolute;
top: 0.2rem;
right: 0.6rem;
font-size: 1.4rem;
cursor: pointer;
opacity: 0.4;
line-height: 1;
background: none;
border: none;
color: inherit;
}
.modal-close:hover {
opacity: 0.8;
}
.tab-bar {
display: flex;
margin: 0 0.6rem;
border-bottom: 2px solid rgba(128,128,128,0.3);
}
.tab-btn {
flex: 1;
padding: 0.5rem;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
color: inherit;
opacity: 0.5;
background: transparent;
border: none;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
}
.tab-btn.active {
opacity: 1;
border-bottom-color: currentColor;
}
.tab-content {
padding: 0.6rem 0;
}
.tab-actions {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-top: 0.8rem;
padding-top: 0.8rem;
border-top: 1px solid rgba(128,128,128,0.2);
}
`, "AutoLogin-Style");
})();
(async function Main($Cookie, $Shared) {
const ACCOUNTS_KEY = "E/Ex_Accounts";
const OLD_KEY = "E/Ex_Cookies";
function migrateOldData() {
if (Lib.getJV(ACCOUNTS_KEY)) return;
const oldCookies = Lib.getJV(OLD_KEY);
if (oldCookies && Array.isArray(oldCookies) && oldCookies.length > 0) {
const now = Lib.getDate("{year}-{month}-{date} {hour}:{minute}");
Lib.setJV(ACCOUNTS_KEY, [{
id: Date.now().toString(),
note: "",
isDefault: true,
cookies: oldCookies,
createdAt: now,
updatedAt: now
}]);
}
}
function getAccounts() {
const data = Lib.getJV(ACCOUNTS_KEY);
if (!data) return [];
let accounts;
if (typeof data === "string") {
try { accounts = JSON.parse(data); } catch (e) { return []; }
} else {
accounts = Array.isArray(data) ? data : [];
}
if (accounts.length > 0 && !accounts.some(a => a.isDefault)) {
accounts[0].isDefault = true;
saveAccounts(accounts);
}
return accounts;
}
function saveAccounts(accounts) {
Lib.setJV(ACCOUNTS_KEY, accounts);
}
function getDefaultAccount() {
const accounts = getAccounts();
return accounts.find(a => a.isDefault) || accounts[0] || null;
}
function addAccount(cookies, note) {
const accounts = getAccounts();
const now = Lib.getDate("{year}-{month}-{date} {hour}:{minute}");
accounts.push({
id: Date.now().toString(),
note: note || "",
isDefault: !accounts.some(a => a.isDefault),
cookies: cookies,
createdAt: now,
updatedAt: now
});
saveAccounts(accounts);
}
function setDefaultAccount(id) {
const accounts = getAccounts();
accounts.forEach(a => a.isDefault = a.id === id);
saveAccounts(accounts);
return accounts;
}
function updateAccountNote(id, note) {
const accounts = getAccounts();
const account = accounts.find(a => a.id === id);
if (account) {
account.note = note;
account.updatedAt = Lib.getDate("{year}-{month}-{date} {hour}:{minute}");
saveAccounts(accounts);
}
return accounts;
}
function deleteAccount(id) {
let accounts = getAccounts();
accounts = accounts.filter(a => a.id !== id);
saveAccounts(accounts);
return accounts;
}
function updateAccountCookies(id, cookies) {
const accounts = getAccounts();
const account = accounts.find(a => a.id === id);
if (account) {
account.cookies = cookies;
account.updatedAt = Lib.getDate("{year}-{month}-{date} {hour}:{minute}");
saveAccounts(accounts);
}
return accounts;
}
migrateOldData();
let Share = Lib.getV("Share", {});
if (typeof Share === "string") {
Share = JSON.parse(Share);
}
const CreateMenu = async Modal => {
Lib.$q(".modal-background")?.remove();
$("body").append(Modal.replace(/>\s+</g, "><"));
requestAnimationFrame(() => {
$(".modal-background").css({
opacity: "1",
"background-color": "rgba(0,0,0,0.7)"
});
});
};
const DeleteMenu = async () => {
const modal = $(".modal-background");
modal.css({
opacity: "0",
"pointer-events": "none",
"background-color": "rgba(0,0,0,0)"
});
setTimeout(() => {
modal.remove();
}, 300);
};
const RegisterMenu = async () => {
Lib.unMenu("Check-1");
Lib.unMenu("Account-1");
Lib.unMenu("Inject-1");
const defaultAccount = getDefaultAccount();
const hasCookie = Boolean(defaultAccount);
const state = Lib.getV("Login", hasCookie);
Lib.regMenu({
[state ? Transl("🟢 Enable Detection") : Transl("🔴 Disable Detection")]: {
func: () => {
if (state) Lib.setV("Login", false); else if (hasCookie) Lib.setV("Login", true); else {
AlertModal(Transl("No Saved Cookies - Cannot Enable Auto-Login"));
return;
}
RegisterMenu();
},
close: false
}
}, {
name: "Check"
});
Lib.regMenu({
[Transl("Inject Default")]: ManualInjection
}, {
name: "Inject"
});
Lib.regMenu({
[Transl("Account Management")]: AccountManagement
}, {
name: "Account"
});
};
const GlobalMenuToggle = async () => {
Lib.storeListen(["Login"], listen => {
listen.far && RegisterMenu();
});
};
async function Injection() {
const defaultAccount = getDefaultAccount();
const login = Lib.getV("Login", Boolean(defaultAccount));
if (login && defaultAccount) {
let CurrentTime = new Date();
let DetectionTime = Lib.local("DetectionTime");
DetectionTime = DetectionTime ? new Date(DetectionTime) : new Date(CurrentTime.getTime() + 11 * 60 * 1e3);
const Conversion = Math.abs(DetectionTime - CurrentTime) / (1e3 * 60);
if (Conversion >= 10) $Cookie.Verify(defaultAccount.cookies);
}
RegisterMenu();
GlobalMenuToggle();
}
async function SharedLogin() {
const Igneous = $Cookie.Get().igneous;
const AccountQuantity = Object.keys(Share).length;
let Select = $(`<select id="account-select" class="acc-select"></select>`), Value;
for (let i = 1; i <= AccountQuantity; i++) {
if (Share[i][0].value === Igneous) Value = i;
Select.append($("<option>").attr({
value: i
}).text(`${Transl("Account")} ${i}`));
}
CreateMenu(`
<div class="modal-background">
<div class="acc-modal">
<h1>${Transl("Account Selection")}</h1>
<div class="acc-select-flex">${Select.prop("outerHTML")}</div>
<div class="acc-button-flex">
<button class="modal-button" id="update">${Transl("Update")}</button>
<button class="modal-button" id="login">${Transl("Login")}</button>
</div>
</div>
</div>
`);
if (AccountQuantity === 0) {
$("#account-select").append($("<option>")).prop("disabled", true);
} else if (Value) $("#account-select").val(Value);
$(".modal-background").on("click", function (click) {
click.stopImmediatePropagation();
const target = click.target;
if (target.id === "login") {
$Cookie.ReAdd(Share[+$("#account-select").val()]);
} else if (target.id === "update") {
$Shared.Update().then(Data => {
if (Data) {
Share = Data;
Lib.setJV("Share", Data);
setTimeout(SharedLogin, 600);
}
});
} else if (target.className === "modal-background") {
DeleteMenu();
}
});
}
async function Cookie_Show(cookies) {
CreateMenu(`
<div class="modal-background">
<div class="show-modal">
<h1 style="text-align: center;">${Transl("Confirm Selected Cookies")}</h1>
<pre><b>${JSON.stringify(cookies, null, 4)}</b></pre>
<div class="set-box">
<label>${Transl("Note")}:</label>
<input class="set-list" type="text" id="cookie-note" placeholder="${Transl("Note")}">
</div>
<div style="text-align: right;">
<button class="modal-button" id="save">${Transl("Confirm Save")}</button>
<button class="modal-button" id="close">${Transl("Cancel")}</button>
</div>
</div>
</div>
`);
$(".modal-background").on("click", function (click) {
click.stopImmediatePropagation();
const target = click.target;
if (target.id === "save") {
const note = $("#cookie-note").val().trim();
addAccount(cookies, note);
Growl(Transl("Save Successful!"), 1500);
DeleteMenu();
} else if (target.className === "modal-background" || target.id === "close") {
DeleteMenu();
}
});
}
async function AutoGetCookie() {
let cookie_box = [];
for (const [name, value] of Object.entries($Cookie.Get())) {
cookie_box.push({
name: name,
value: value
});
}
cookie_box.length > 1 ? Cookie_Show(cookie_box) : AlertModal(Transl("No Cookies Retrieved!\n\nPlease Login First"));
}
async function ManualSetting() {
CreateMenu(`
<div class="modal-background">
<div class="set-modal">
<h1>${Transl("Set Cookies")}</h1>
<form id="set_cookies">
<div id="input_cookies" class="set-box">
<label>${Transl("Note")}:</label><input class="set-list" type="text" id="manual-note" placeholder="${Transl("Note")}"><br>
<label>[igneous]:</label><input class="set-list" type="text" name="igneous" placeholder="${Transl("Required for Ex Login Only")}"><br>
<label>[ipb_member_id]:</label><input class="set-list" type="text" name="ipb_member_id" placeholder="${Transl("Required Field")}" required><br>
<label>[ipb_pass_hash]:</label><input class="set-list" type="text" name="ipb_pass_hash" placeholder="${Transl("Required Field")}" required><hr>
<h3>${Transl("Optional Fields Below")}</h3>
<label>[sl]:</label><input class="set-list" type="text" name="sl" value="dm_2"><br>
<label>[sk]:</label><input class="set-list" type="text" name="sk"><br>
</div>
<button type="submit" class="modal-button" id="save">${Transl("Confirm Save")}</button>
<button class="modal-button" id="close">${Transl("Exit Menu")}</button>
</form>
</div>
</div>
`);
let cookie;
const textarea = $("<textarea>").attr({
style: "margin: 1.15rem auto 0 auto",
rows: 18,
cols: 40,
readonly: true
});
$("#set_cookies").on("submit", function (submit) {
submit.preventDefault();
submit.stopImmediatePropagation();
cookie = Array.from($("#set_cookies .set-list")).map(function (input) {
if (input.id === "manual-note") return null;
const value = $(input).val();
return value.trim() !== "" ? {
name: $(input).attr("name"),
value: value
} : null;
}).filter(Boolean);
textarea.val(JSON.stringify(cookie, null, 4));
$("#set_cookies div").append(textarea);
Growl(Transl("[Confirm Input Correct] Press Exit to Save"), 2500);
});
$(".modal-background").on("click", function (click) {
click.stopImmediatePropagation();
const target = click.target;
if (target.className === "modal-background" || target.id === "close") {
click.preventDefault();
if (target.id === "close" && cookie) {
const note = $("#manual-note").val().trim();
addAccount(cookie, note);
}
DeleteMenu();
}
});
}
async function AccountManagement() {
let accounts = getAccounts();
const savedTab = Lib.getV("AccountTab", "local");
let currentTab = savedTab;
const renderLocalTab = () => {
const container = $("#account-list");
container.empty();
if (accounts.length === 0) {
container.append(`<p style="text-align:center;opacity:0.6;">${Transl("No accounts saved")}</p>`);
renderCurrentLogin();
return;
}
accounts.forEach((account, index) => {
const isDefault = account.isDefault;
const item = $(`
<div class="account-item" data-id="${account.id}">
<span class="account-index">#${index + 1}${isDefault ? ` (${Transl("Default")})` : ""}</span>
<input class="account-note" placeholder="${Transl("Note")}">
<button class="account-action view-cookies">${account.cookies.length} cookies</button>
<button class="account-action set-default" ${isDefault ? "disabled" : ""}>${Transl("Set as Default")}</button>
<button class="account-action inject-account">${Transl("Inject")}</button>
</div>
`);
item.find(".account-note").val(account.note || "");
container.append(item);
});
renderCurrentLogin();
};
const renderCurrentLogin = () => {
const el = $("#current-login");
if (!el.length) return;
const currentId = $Cookie.Get().ipb_member_id;
if (!currentId) {
el.text(`${Transl("Current Login")}: ${Transl("Not logged in")}`);
return;
}
const matches = getAccounts()
.map((a, i) => ({ i: i + 1, note: a.note || "", id: (a.cookies || []).find(c => c.name === "ipb_member_id")?.value }))
.filter(m => m.id != null && m.id === currentId);
let text = `${Transl("Current Login")}: ${currentId}`;
if (matches.length) {
text += ` (${matches.map(m => `#${m.i} ${m.note || "user"}`).join(", ")})`;
}
el.text(text);
};
const renderSharedTab = () => {
const container = $("#shared-content");
container.empty();
const Igneous = $Cookie.Get().igneous;
const AccountQuantity = Object.keys(Share).length;
let Select = $(`<select id="share-select" class="acc-select"></select>`), Value;
for (let i = 1; i <= AccountQuantity; i++) {
if (Share[i][0].value === Igneous) Value = i;
Select.append($("<option>").attr({ value: i }).text(`${Transl("Account")} ${i}`));
}
container.append(`
<div class="set-box">
<label>${Transl("Shared Data URL")}:</label>
<input class="set-list" type="url" id="share-url" value="${Lib.getV("ShareURL", "").replace(/&/g, "&").replace(/"/g, """)}">
</div>
<div class="acc-select-flex">${Select.prop("outerHTML")}</div>
<div class="acc-button-flex">
<button class="modal-button" id="share-update">${Transl("Update")}</button>
<button class="modal-button" id="share-login">${Transl("Login")}</button>
</div>
`);
if (AccountQuantity === 0) {
$("#share-select").append($("<option>")).prop("disabled", true);
} else if (Value) $("#share-select").val(Value);
};
const switchTab = (tab) => {
currentTab = tab;
Lib.setV("AccountTab", tab);
$(".tab-btn").removeClass("active");
$(`#tab-${tab}`).addClass("active");
$(".tab-content").hide();
$(`#tab-content-${tab}`).show();
};
const showCookies = account => {
const index = getAccounts().findIndex(a => a.id === account.id);
const label = account.note || `${Transl("Account")} ${index + 1}`;
const modal = $(`
<div class="modal-background">
<div class="show-modal">
<h1 style="text-align:center;">${label}</h1>
<textarea id="cookie-editor" rows="14" spellcheck="false" style="width:100%;box-sizing:border-box;resize:vertical;font-family:monospace;font-size:1rem;background:transparent;color:inherit;">${JSON.stringify(account.cookies, null, 4).replace(/&/g, "&").replace(/</g, "<")}</textarea>
<div style="text-align:right;">
<button class="modal-button" id="cookie-delete">${Transl("Delete")}</button>
<button class="modal-button" id="cookie-close">${Transl("Close")}</button>
<button class="modal-button" id="cookie-save">${Transl("Save")}</button>
</div>
</div>
</div>
`).appendTo("body");
requestAnimationFrame(() => modal.css({ opacity: "1", "background-color": "rgba(0,0,0,0.7)" }));
const close = () => {
modal.css({ opacity: "0", "pointer-events": "none", "background-color": "rgba(0,0,0,0)" });
setTimeout(() => modal.remove(), 300);
};
modal.on("click", async function (click) {
const target = click.target;
if (target.id === "cookie-close" || target.className === "modal-background") {
close();
} else if (target.id === "cookie-save") {
try {
const parsed = JSON.parse(modal.find("#cookie-editor").val());
if (!Array.isArray(parsed)) throw new Error();
accounts = updateAccountCookies(account.id, parsed);
close();
Growl(Transl("Changes Saved"), 1500);
} catch (error) {
modal.find(".cookie-error").remove();
modal.find("h1").after(`<p class="cookie-error" style="color:#e0483d;">${Transl("Invalid JSON")}</p>`);
}
} else if (target.id === "cookie-delete") {
if (await ConfirmModal(Transl("Are you sure you want to delete this account?"))) {
accounts = deleteAccount(account.id);
renderLocalTab();
close();
Growl(Transl("Account deleted"), 1500);
}
}
});
};
CreateMenu(`
<div class="modal-background">
<div class="set-modal" style="width:50%;">
<h1>${Transl("Account Management")}</h1>
<div class="current-login" id="current-login"></div>
<div class="tab-bar">
<button id="tab-local" class="tab-btn">${Transl("Local Accounts")}</button>
<button id="tab-shared" class="tab-btn">${Transl("Shared Accounts")}</button>
</div>
<div id="tab-content-local" class="tab-content">
<div class="account-list" id="account-list"></div>
<div class="tab-actions">
<button class="modal-button" id="auto-get">${Transl("Auto Retrieve")}</button>
<button class="modal-button" id="manual-set">${Transl("Manual Input")}</button>
<button class="modal-button" id="clear-login">${Transl("Clear Login")}</button>
</div>
</div>
<div id="tab-content-shared" class="tab-content">
<div id="shared-content"></div>
</div>
<button class="modal-button" id="close">${Transl("Exit Menu")}</button>
</div>
</div>
`);
switchTab(currentTab);
renderLocalTab();
renderSharedTab();
$("#tab-local").on("click", () => switchTab("local"));
$("#tab-shared").on("click", () => switchTab("shared"));
$("#auto-get").on("click", () => { DeleteMenu(); setTimeout(AutoGetCookie, 100); });
$("#manual-set").on("click", () => { DeleteMenu(); setTimeout(ManualSetting, 100); });
$("#clear-login").on("click", async () => {
if (await ConfirmModal(Transl("Are you sure you want to clear current login information? This will not affect saved cookies."))) {
DeleteMenu();
ClearLogin();
}
});
$("#share-login").on("click", () => {
$Cookie.ReAdd(Share[+$("#share-select").val()]);
});
$("#share-update").on("click", () => {
$Shared.Update().then(Data => {
if (Data) {
Share = Data;
Lib.setJV("Share", Data);
renderSharedTab();
}
});
});
$(".modal-background").on("input", ".account-note", function () {
const id = $(this).closest(".account-item").attr("data-id");
accounts = updateAccountNote(id, $(this).val().trim());
});
$(".modal-background").on("input", "#share-url", function () {
Lib.setV("ShareURL", $(this).val().trim());
});
$(".modal-background").on("click", "button.view-cookies", function () {
const id = $(this).closest(".account-item").attr("data-id");
const account = accounts.find(a => a.id === id);
if (account) showCookies(account);
});
$(".modal-background").on("click", "button.inject-account", function () {
const id = $(this).closest(".account-item").attr("data-id");
const account = accounts.find(a => a.id === id);
if (account) $Cookie.ReAdd(account.cookies);
});
$(".modal-background").on("click", "button.set-default", function () {
const id = $(this).closest(".account-item").attr("data-id");
accounts = setDefaultAccount(id);
renderLocalTab();
});
$(".modal-background").on("click", function (click) {
if (click.target.className === "modal-background" || click.target.id === "close") {
DeleteMenu();
}
});
}
async function ManualInjection() {
const defaultAccount = getDefaultAccount();
if (!defaultAccount) {
AlertModal(Transl("No Injectable Cookies!\n\nConfigure in Menu"));
return;
}
$Cookie.ReAdd(defaultAccount.cookies);
}
async function ClearLogin() {
$Cookie.Delete();
location.reload();
}
return {
Injection: Injection
};
})(CookieFactory(), SharedFactory()).then(Main => {
Main.Injection();
});
function Growl(message, life = 2000) {
let container = $(".toast-container");
if (!container.length) {
container = $('<div class="toast-container"></div>').appendTo("body");
}
const toast = $('<div class="toast"></div>').text(message).appendTo(container);
requestAnimationFrame(() => toast.addClass("show"));
setTimeout(() => {
toast.removeClass("show");
setTimeout(() => toast.remove(), 250);
}, life);
}
function ModalLayer(inner, ids) {
return new Promise(resolve => {
const modal = $(
`<div class="modal-background">
<div class="show-modal" style="text-align:center;">
<p style="margin:1rem 0;">${inner}</p>
<div style="text-align:right;">
<button class="modal-button" id="${ids[0]}">${Transl("Cancel")}</button>
${ids[1] ? `<button class="modal-button" id="${ids[1]}">${Transl("Confirm")}</button>` : ""}
</div>
</div>
</div>`
).appendTo("body");
requestAnimationFrame(() => modal.css({ opacity: "1", "background-color": "rgba(0,0,0,0.7)" }));
const close = result => {
modal.css({ opacity: "0", "pointer-events": "none", "background-color": "rgba(0,0,0,0)" });
setTimeout(() => modal.remove(), 300);
resolve(result);
};
modal.on("click", function (click) {
const target = click.target;
if (target.id === ids[1]) close(true);
else if (target.id === ids[0] || target.className === "modal-background") close(ids[1] ? false : true);
});
});
}
const AlertModal = message => ModalLayer(message, ["alert-ok"]);
const ConfirmModal = message => ModalLayer(message, ["confirm-no", "confirm-yes"]);
function SharedFactory() {
async function Get() {
const url = Lib.getV("ShareURL", "");
if (!url) {
AlertModal(Transl("Please set the Shared Data URL first"));
return {};
}
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
responseType: "json",
url: url,
onload: response => {
if (response.status === 200) {
const data = response.response;
if (typeof data === "object" && Object.keys(data).length > 0) {
resolve(data);
} else {
console.error(Transl("Request Empty Data"));
resolve({});
}
} else {
console.error(Transl("Connection Error - Update Address May Be Wrong"));
resolve({});
}
},
onerror: error => {
console.error(Transl("Request Error: "), error);
resolve({});
}
});
});
}
async function Update() {
const Shared = await Get();
if (Object.keys(Shared).length > 0) {
const localHash = md5(Lib.getV("Share", ""));
const remoteHash = md5(JSON.stringify(Shared));
if (localHash !== remoteHash) {
Growl(Transl("Shared Data Update Complete"), 1500);
return Shared;
} else {
Growl(Transl("Shared Data Update Not Needed"), 1500);
}
} else {
Growl(Transl("Shared Data Retrieval Failed"), 2500);
}
return false;
}
return {
Update: Update
};
}
function CookieFactory() {
const Today = new Date();
Today.setFullYear(Today.getFullYear() + 1);
const Expires = Today.toUTCString();
const UnixUTC = new Date(0).toUTCString();
let RequiredCookie = ["ipb_member_id", "ipb_pass_hash"];
if (domain == "exhentai.org") RequiredCookie.unshift("igneous");
return {
Get: () => {
return Lib.cookie().split("; ").reduce((acc, cookie) => {
const [name, value] = cookie.split("=");
acc[decodeURIComponent(name)] = decodeURIComponent(value);
return acc;
}, {});
},
Add: function (CookieObject) {
Lib.local("DetectionTime", {
value: Lib.getDate()
});
for (const Cookie of CookieObject) {
Lib.cookie(`${encodeURIComponent(Cookie.name)}=${encodeURIComponent(Cookie.value)}; domain=.${domain}; path=/; expires=${Expires};`);
}
location.reload();
},
Delete: function () {
Object.keys(this.Get()).forEach(Name => {
Lib.cookie(`${Name}=; expires=${UnixUTC}; path=/;`);
Lib.cookie(`${Name}=; expires=${UnixUTC}; path=/; domain=.${domain}`);
});
},
ReAdd: function (Cookies) {
this.Delete();
this.Add(Cookies);
},
Verify: function (Cookies) {
const Cookie = this.Get();
const VCookie = new Set(Object.keys(Cookie));
const Result = RequiredCookie.every(key => VCookie.has(key) && Cookie[key] !== "mystery");
if (!Result) {
this.ReAdd(Cookies);
} else {
Lib.local("DetectionTime", {
value: Lib.getDate()
});
}
}
};
}
function Language() {
const Word = Lib.translMatcher({
English: {},
Traditional: {
"Auto Retrieve": "自動獲取",
"Manual Input": "手動輸入",
"Inject Default": "注入默認",
"Clear Login": "清除登入",
"🟢 Enable Detection": "🟢 啟用檢測",
"🔴 Disable Detection": "🔴 禁用檢測",
"Account": "帳戶",
"Update": "更新",
"Login": "登入",
"Confirm Selected Cookies": "確認選擇的 Cookies",
"Confirm Save": "確認保存",
"Cancel": "取消退出",
"Exit Menu": "退出選單",
"Save Successful!": "保存成功!",
"Set Cookies": "設置 Cookies",
"Required for Ex Login Only": "要登入 Ex 才需要填寫",
"Required Field": "必填項目",
"Optional Fields Below": "下方選填 也可不修改",
"[Confirm Input Correct] Press Exit to Save": "[確認輸入正確] 按下退出選單保存",
"Current Cookie Settings": "當前設置 Cookies",
"Account Selection": "帳戶選擇",
"No Cookies Retrieved!\n\nPlease Login First": "未獲取到 Cookies !!\n\n請先登入帳戶",
"No Injectable Cookies!\n\nConfigure in Menu": "未檢測到可注入的 Cookies !!\n\n請從選單中進行設置",
"Shared Data Update Complete": "共享數據更新完成",
"Shared Data Update Not Needed": "共享數據無需更新",
"Shared Data Retrieval Failed": "共享數據獲取失敗",
"No Saved Cookies - Cannot Enable Auto-Login": "無保存的 Cookie, 無法啟用自動登入",
"Request Empty Data": "請求為空數據",
"Connection Error - Update Address May Be Wrong": "連線異常,更新地址可能是錯的",
"Request Error: ": "請求錯誤: ",
"Note": "備註",
"Default": "默認",
"No accounts saved": "無已保存賬號",
"Delete": "刪除",
"Close": "關閉",
"Inject": "注入",
"Account deleted": "賬號已刪除",
"Account Management": "賬號管理",
"Current Login": "當前登入",
"Not logged in": "未登入",
"Local Accounts": "本地賬號",
"Shared Accounts": "共享賬號",
"Set as Default": "設為默認",
"Are you sure you want to delete this account?": "確定要刪除此賬號嗎?",
"Are you sure you want to clear current login information? This will not affect saved cookies.": "確定要清除當前登入信息嗎?這不會影響已保存的 cookies。",
"Shared Data URL": "共享數據URL",
"Please set the Shared Data URL first": "請先設置共享數據URL"
},
Simplified: {
"Auto Retrieve": "自动获取",
"Manual Input": "手动输入",
"Inject Default": "注入默认",
"Clear Login": "清除登录信息",
"🟢 Enable Detection": "🟢 启用检测",
"🔴 Disable Detection": "🔴 禁用检测",
"Account": "账号",
"Update": "更新",
"Login": "登录",
"Confirm Selected Cookies": "确认所选 Cookies",
"Confirm Save": "确认保存",
"Cancel": "取消",
"Exit Menu": "关闭菜单",
"Save Successful!": "保存成功!",
"Set Cookies": "设置 Cookies",
"Required for Ex Login Only": "仅登录 Ex 时需要填写",
"Required Field": "必填项",
"Optional Fields Below": "以下为选填项,可不修改",
"[Confirm Input Correct] Press Exit to Save": "[确认输入无误] 点击关闭菜单保存",
"Current Cookie Settings": "当前 Cookies 设置",
"Account Selection": "选择账号",
"No Cookies Retrieved!\n\nPlease Login First": "未获取到 Cookies!\n\n请先登录账号",
"No Injectable Cookies!\n\nConfigure in Menu": "未检测到可注入的 Cookies!\n\n请在菜单中进行设置",
"Shared Data Update Complete": "共享数据更新完成",
"Shared Data Update Not Needed": "共享数据无需更新",
"Shared Data Retrieval Failed": "共享数据获取失败",
"No Saved Cookies - Cannot Enable Auto-Login": "没有已保存的 Cookie,无法启用自动登录",
"Request Empty Data": "请求数据为空",
"Connection Error - Update Address May Be Wrong": "连接异常,更新地址可能不正确",
"Request Error: ": "请求错误:",
"Note": "备注",
"Default": "默认",
"No accounts saved": "无已保存账号",
"Delete": "删除",
"Close": "关闭",
"Inject": "注入",
"Account deleted": "账号已删除",
"Account Management": "账号管理",
"Current Login": "当前登入",
"Not logged in": "未登录",
"Local Accounts": "本地账号",
"Shared Accounts": "共享账号",
"Set as Default": "设为默认",
"Are you sure you want to delete this account?": "确定要删除此账号吗?",
"Are you sure you want to clear current login information? This will not affect saved cookies.": "确定要清除当前登入信息吗?这不会影响已保存的cookies。",
"Shared Data URL": "共享数据URL",
"Please set the Shared Data URL first": "请先设置共享数据URL"
},
Japan: {
"Auto Retrieve": "自動取得",
"Manual Input": "手動入力",
"Inject Default": "デフォルト注入",
"Clear Login": "ログインをクリア",
"🟢 Enable Detection": "🟢 検出を有効化",
"🔴 Disable Detection": "🔴 検出を無効化",
"Account": "アカウント",
"Update": "更新",
"Login": "ログイン",
"Confirm Selected Cookies": "選択したCookieを確認",
"Confirm Save": "保存を確認",
"Cancel": "終了をキャンセル",
"Exit Menu": "メニューを終了",
"Save Successful!": "保存に成功しました!",
"Set Cookies": "Cookieを設定",
"Required for Ex Login Only": "Exログインにのみ必要",
"Required Field": "必須項目",
"Optional Fields Below": "以下は任意、変更しなくても構いません",
"[Confirm Input Correct] Press Exit to Save": "[入力が正しいことを確認] メニュー終了を押して保存",
"Current Cookie Settings": "現在のCookie設定",
"Account Selection": "アカウント選択",
"No Cookies Retrieved!\n\nPlease Login First": "Cookieを取得できませんでした!\n\nまずアカウントにログインしてください",
"No Injectable Cookies!\n\nConfigure in Menu": "注入可能なCookieが検出されませんでした!\n\nメニューから設定してください",
"Shared Data Update Complete": "共有データの更新が完了しました",
"Shared Data Update Not Needed": "共有データの更新は不要です",
"Shared Data Retrieval Failed": "共有データの取得に失敗しました",
"No Saved Cookies - Cannot Enable Auto-Login": "保存されたCookieがないため、自動ログインを有効にできません",
"Request Empty Data": "リクエストにデータがありません",
"Connection Error - Update Address May Be Wrong": "接続エラー、更新アドレスが間違っている可能性があります",
"Request Error: ": "リクエストエラー: ",
"Note": "メモ",
"Default": "デフォルト",
"No accounts saved": "保存されたアカウントがありません",
"Delete": "削除",
"Close": "閉じる",
"Inject": "注入",
"Account deleted": "アカウントを削除しました",
"Account Management": "アカウント管理",
"Current Login": "現在ログイン中",
"Not logged in": "未ログイン",
"Local Accounts": "ローカルアカウント",
"Shared Accounts": "共有アカウント",
"Set as Default": "デフォルトに設定",
"Are you sure you want to delete this account?": "このアカウントを削除してもよろしいですか?",
"Are you sure you want to clear current login information? This will not affect saved cookies.": "現在のログイン情報を消去してもよろしいですか?保存済みのCookieには影響しません。",
"Shared Data URL": "共有データURL",
"Please set the Shared Data URL first": "まず共有データURLを設定してください"
},
Korea: {
"Auto Retrieve": "자동 가져오기",
"Manual Input": "수동 입력",
"Inject Default": "기본 주입",
"Clear Login": "로그인 정보 삭제",
"🟢 Enable Detection": "🟢 감지 활성화",
"🔴 Disable Detection": "🔴 감지 비활성화",
"Account": "계정",
"Update": "업데이트",
"Login": "로그인",
"Confirm Selected Cookies": "선택한 쿠키 확인",
"Confirm Save": "저장 확인",
"Cancel": "종료 취소",
"Exit Menu": "메뉴 종료",
"Save Successful!": "저장 성공!",
"Set Cookies": "쿠키 설정",
"Required for Ex Login Only": "Ex 로그인에만 필요",
"Required Field": "필수 항목",
"Optional Fields Below": "아래는 선택사항, 변경하지 않아도 됩니다",
"[Confirm Input Correct] Press Exit to Save": "[입력이 정확한지 확인] 메뉴 종료를 눌러 저장",
"Current Cookie Settings": "현재 설정된 쿠키",
"Account Selection": "계정 선택",
"No Cookies Retrieved!\n\nPlease Login First": "쿠키를 가져오지 못했습니다!\n\n먼저 계정에 로그인해 주세요",
"No Injectable Cookies!\n\nConfigure in Menu": "주입 가능한 쿠키가 감지되지 않았습니다!\n\n메뉴에서 설정해 주세요",
"Shared Data Update Complete": "공유 데이터 업데이트 완료",
"Shared Data Update Not Needed": "공유 데이터 업데이트 불필요",
"Shared Data Retrieval Failed": "공유 데이터 가져오기 실패",
"No Saved Cookies - Cannot Enable Auto-Login": "저장된 쿠키가 없어 자동 로그인을 활성화할 수 없습니다",
"Request Empty Data": "요청에 데이터가 없습니다",
"Connection Error - Update Address May Be Wrong": "연결 오류, 업데이트 주소가 잘못되었을 수 있습니다",
"Request Error: ": "요청 오류: ",
"Note": "메모",
"Default": "기본",
"No accounts saved": "저장된 계정이 없습니다",
"Delete": "삭제",
"Close": "닫기",
"Inject": "주입",
"Account deleted": "계정이 삭제되었습니다",
"Account Management": "계정 관리",
"Current Login": "현재 로그인",
"Not logged in": "로그인 안 됨",
"Local Accounts": "로컬 계정",
"Shared Accounts": "공유 계정",
"Set as Default": "기본값으로 설정",
"Are you sure you want to delete this account?": "이 계정을 삭제하시겠습니까?",
"Are you sure you want to clear current login information? This will not affect saved cookies.": "현재 로그인 정보를 삭제하시겠습니까? 저장된 쿠키에는 영향을 주지 않습니다.",
"Shared Data URL": "공유 데이터 URL",
"Please set the Shared Data URL first": "먼저 공유 데이터 URL을 설정해 주세요"
},
Russia: {
"Auto Retrieve": "Автоматическое получение",
"Manual Input": "Ручной ввод",
"Inject Default": "Внедрить по умолчанию",
"Clear Login": "Очистить вход",
"🟢 Enable Detection": "🟢 Включить обнаружение",
"🔴 Disable Detection": "🔴 Отключить обнаружение",
"Account": "Аккаунт",
"Update": "Обновить",
"Login": "Войти",
"Confirm Selected Cookies": "Подтвердить выбранные Cookies",
"Confirm Save": "Подтвердить сохранение",
"Cancel": "Отменить выход",
"Exit Menu": "Выйти из меню",
"Save Successful!": "Сохранение успешно!",
"Set Cookies": "Настройка Cookies",
"Required for Ex Login Only": "Требуется только для входа в Ex",
"Required Field": "Обязательное поле",
"Optional Fields Below": "Необязательно ниже, изменения не требуются",
"[Confirm Input Correct] Press Exit to Save": "[Подтвердите правильность ввода] Нажмите Выйти из меню для сохранения",
"Current Cookie Settings": "Текущие настройки Cookies",
"Account Selection": "Выбор аккаунта",
"No Cookies Retrieved!\n\nPlease Login First": "Cookies не получены !!\n\nПожалуйста, сначала войдите в аккаунт",
"No Injectable Cookies!\n\nConfigure in Menu": "Не обнаружены Cookies для внедрения !!\n\nПожалуйста, настройте в меню",
"Shared Data Update Complete": "Обновление общих данных завершено",
"Shared Data Update Not Needed": "Обновление общих данных не требуется",
"Shared Data Retrieval Failed": "Ошибка получения общих данных",
"No Saved Cookies - Cannot Enable Auto-Login": "Нет сохраненных cookies, невозможно включить автоматический вход",
"Request Empty Data": "Запрос содержит пустые данные",
"Connection Error - Update Address May Be Wrong": "Ошибка соединения, адрес обновления может быть неверным",
"Request Error: ": "Ошибка запроса: ",
"Note": "Заметка",
"Default": "По умолчанию",
"No accounts saved": "Нет сохраненных аккаунтов",
"Delete": "Удалить",
"Close": "Закрыть",
"Inject": "Внедрить",
"Account deleted": "Аккаунт удален",
"Account Management": "Управление аккаунтами",
"Current Login": "Текущий вход",
"Not logged in": "Не выполнен",
"Local Accounts": "Локальные аккаунты",
"Shared Accounts": "Общие аккаунты",
"Set as Default": "Сделать основным",
"Are you sure you want to delete this account?": "Вы уверены, что хотите удалить этот аккаунт?",
"Are you sure you want to clear current login information? This will not affect saved cookies.": "Вы уверены, что хотите очистить текущие данные входа? Это не повлияет на сохраненные cookies.",
"Shared Data URL": "URL общих данных",
"Please set the Shared Data URL first": "Сначала укажите URL общих данных"
}
});
return {
Transl: Str => Word[Str] ?? Str
};
}
})();