// ==UserScript==
// @name pppixiv for Pixiv
// @author rainbowflesh, ppixiv
// @namespace pppixiv
// @description PPixiv Plus, the PPixiv overhaul.
// @homepage https://github.com/rainbowflesh/pppixiv
// @match https://*.pixiv.net/*
// @icon https://www.pixiv.net/favicon.ico
// @license MIT
// @run-at document-start
// @grant GM.xmlHttpRequest
// @grant GM.setValue
// @grant GM.getValue
// @connect pixiv.net
// @connect pximg.net
// @connect self
// @connect *
// @version 20250524-78c7a065
// ==/UserScript==
(function() {
// The script is packaged into a string so we can execute it outside of the
// userscript sandbox to avoid various problems with script managers.
let bundle = `(() => {
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
var __toBinary = /* @__PURE__ */ (() => {
var table = new Uint8Array(128);
for (var i = 0; i < 64; i++)
table[i < 26 ? i + 65 : i < 52 ? i + 71 : i < 62 ? i - 4 : i * 4 - 205] = i;
return (base64) => {
var n = base64.length, bytes = new Uint8Array((n - (base64[n - 1] == "=") - (base64[n - 2] == "=")) * 3 / 4 | 0);
for (var i2 = 0, j = 0; i2 < n; ) {
var c0 = table[base64.charCodeAt(i2++)], c1 = table[base64.charCodeAt(i2++)];
var c2 = table[base64.charCodeAt(i2++)], c3 = table[base64.charCodeAt(i2++)];
bytes[j++] = c0 << 2 | c1 >> 4;
bytes[j++] = c1 << 4 | c2 >> 2;
bytes[j++] = c2 << 6 | c3;
}
return bytes;
};
})();
/\x2f web/vview/misc/polyfills.js
function installPolyfills() {
if (!IDBRequest.prototype[Symbol.asyncIterator]) {
IDBRequest.prototype[Symbol.asyncIterator] = function() {
return {
next: () => {
return new Promise((accept, reject) => {
this.onsuccess = (e) => {
let entry = e.target.result;
if (entry == null) {
accept({ done: true });
return;
}
accept({ value: entry, done: false });
entry.continue();
};
this.onerror = (e) => {
reject(e);
};
});
}
};
};
}
Animation.prototype.commitStylesIfPossible = function() {
try {
this.commitStyles();
return true;
} catch (e) {
console.error(e);
return false;
}
};
if (!("inert" in document.documentElement)) {
Object.defineProperty(HTMLElement.prototype, "inert", {
get: function() {
return this.hasAttribute("inert");
},
set: function(value) {
if (value)
this.setAttribute("inert", "inert");
else
this.removeAttribute("inert", "inert");
}
});
}
if (ppixiv.ios)
document.addEventListener("dblclick", (e) => {
});
if (!("throwIfAborted" in AbortSignal.prototype)) {
AbortSignal.prototype.throwIfAborted = function() {
if (this.aborted)
throw new DOMException(this.reason ?? "Signal was aborted");
};
}
}
/\x2f web/vview/misc/helpers.js
var helpers_exports = {};
__export(helpers_exports, {
ClassFlags: () => ClassFlags,
FixedDOMRect: () => FixedDOMRect,
GuardedRunner: () => GuardedRunner,
KeyListener: () => KeyListener,
OpenWidgets: () => OpenWidgets,
PointerEventMovement: () => PointerEventMovement,
SafetyBackoffTimer: () => SafetyBackoffTimer,
Timer: () => Timer,
helpers: () => helpers
});
/\x2f web/vview/util/math.js
var math_exports = {};
__export(math_exports, {
clamp: () => clamp,
distance: () => distance,
roundUpTo: () => roundUpTo,
scale: () => scale,
scaleClamp: () => scaleClamp
});
function scale(x, l1, h1, l2, h2) {
return (x - l1) * (h2 - l2) / (h1 - l1) + l2;
}
function clamp(value, min, max) {
if (min > max)
[min, max] = [max, min];
return Math.min(Math.max(value, min), max);
}
function scaleClamp(x, l1, h1, l2, h2) {
return clamp(scale(x, l1, h1, l2, h2), l2, h2);
}
function roundUpTo(i, interval2) {
return Math.floor((i + interval2 - 1) / interval2) * interval2;
}
function distance({ x: x1, y: y1 }, { x: x2, y: y2 }) {
let distance2 = Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2);
return Math.pow(distance2, 0.5);
}
/\x2f web/vview/util/strings.js
var strings_exports = {};
__export(strings_exports, {
ageToString: () => ageToString,
containsAsianText: () => containsAsianText,
dateToString: () => dateToString,
formatSeconds: () => formatSeconds,
getExtension: () => getExtension,
getPathPart: () => getPathPart,
getPathSuffix: () => getPathSuffix,
parseRange: () => parseRange,
parseRatio: () => parseRatio,
setPathPart: () => setPathPart,
titleCase: () => titleCase
});
function getExtension(fn) {
let parts = fn.split(".");
return parts[parts.length - 1];
}
function dateToString(date) {
date = new Date(date);
let day = date.toLocaleDateString();
let time = date.toLocaleTimeString();
return day + " " + time;
}
function titleCase(s) {
let parts = [];
for (let part of s.split(" "))
parts.push(part.substr(0, 1).toUpperCase() + s.substr(1));
return parts.join(" ");
}
function formatSeconds(totalSeconds) {
totalSeconds = Math.floor(totalSeconds);
let result = "";
let seconds = totalSeconds % 60;
totalSeconds = Math.floor(totalSeconds / 60);
let minutes = totalSeconds % 60;
totalSeconds = Math.floor(totalSeconds / 60);
let hours = totalSeconds % 24;
result = \`\${minutes}:\${seconds.toString().padStart(2, "0")}\`;
if (hours > 0) {
result = result.padStart(5, "0");
result = hours + ":" + result;
}
return result;
}
function ageToString(seconds) {
let future = seconds < 0;
if (future)
seconds = -seconds;
function to_plural(label, places, value) {
let factor = Math.pow(10, places);
let plural_value = Math.round(value * factor);
if (plural_value > 1)
label += "s";
let result = value.toFixed(places) + " " + label;
result += future ? " from now" : " ago";
return result;
}
;
if (seconds < 60)
return to_plural("sec", 0, seconds);
let minutes = seconds / 60;
if (minutes < 60)
return to_plural("min", 0, minutes);
let hours = minutes / 60;
if (hours < 24)
return to_plural("hour", 0, hours);
let days = hours / 24;
if (days < 30)
return to_plural("day", 0, days);
let months = days / 30;
if (months < 12)
return to_plural("month", 0, months);
let years = months / 12;
return to_plural("year", 1, years);
}
function parseRatio(value) {
if (value == null || value == "")
return null;
if (value.indexOf == null)
return value;
let parts = value.split(":", 2);
if (parts.length == 1) {
return parseFloat(parts[0]);
} else {
let num = parseFloat(parts[0]);
let den = parseFloat(parts[1]);
return num / den;
}
}
function parseRange(range) {
if (range == null)
return null;
let parts = range.split("...");
let min = parseRatio(parts[0]);
let max = parseRatio(parts[1]);
return [min, max];
}
function getPathSuffix(path, count = 2, remove_from_end = 0, { remove_extension = true } = {}) {
let parts = path.split("/");
parts = parts.splice(0, parts.length - remove_from_end);
parts = parts.splice(parts.length - count);
let result = parts.join("/");
if (remove_extension)
result = result.replace(/\\.[a-z0-9]+\$/i, "");
return result;
}
function setPathPart(url, index, value) {
url = new URL(url);
let parts = url.pathname.split("/");
index++;
if (parts.length > 1 && parts[1].length == 2)
index++;
while (parts.length < index)
parts.push("");
parts[index] = value;
if (value == "" && parts.length == index + 1)
parts = parts.slice(0, index);
url.pathname = parts.join("/");
return url;
}
function getPathPart(url, index, value) {
index++;
let parts = url.pathname.split("/");
if (parts.length > 1 && parts[1].length == 2)
index++;
return parts[index] || "";
}
function containsAsianText(text) {
const cjkRanges = [
[19968, 40959],
[13312, 19903],
[131072, 173791],
[173824, 177983],
[177984, 178207],
[178208, 183983],
[12032, 12255],
[11904, 12031],
[12288, 12351],
[12736, 12783],
[63744, 64255],
[65072, 65103],
[65280, 65519],
[44032, 55215]
];
function isCJK(charCode) {
return cjkRanges.some(([start, end]) => charCode >= start && charCode <= end);
}
for (let i = 0; i < text.length; i++) {
if (isCJK(text.charCodeAt(i)))
return true;
}
return false;
}
/\x2f web/vview/util/html.js
var html_exports = {};
__export(html_exports, {
addStyle: () => addStyle,
createFromTemplate: () => createFromTemplate,
createStyle: () => createStyle,
getRelativePosition: () => getRelativePosition,
getSafeAreaInsets: () => getSafeAreaInsets,
isAbove: () => isAbove,
moveChildren: () => moveChildren,
removeElements: () => removeElements,
restoreScrollPosition: () => restoreScrollPosition,
saveScrollPosition: () => saveScrollPosition,
setClass: () => setClass,
setDataSet: () => setDataSet,
setSizeAsProperty: () => setSizeAsProperty
});
function moveChildren(parent, newParent) {
for (let child of Array.from(parent.children)) {
child.remove();
newParent.appendChild(child);
}
}
function removeElements(parent) {
for (let child of Array.from(parent.children))
child.remove();
}
function isAbove(ancestor, descendant) {
console.assert(ancestor != null, "ancestor is null");
console.assert(descendant != null, "descendant is null");
while (descendant != null && descendant != ancestor)
descendant = descendant.parentNode;
return descendant == ancestor;
}
function createStyle(css, { id } = {}) {
let style = document.realCreateElement("style");
style.type = "text/css";
if (id)
style.id = id;
style.textContent = css;
return style;
}
function addStyle(name, css) {
let style = this.createStyle(css);
style.id = name;
document.querySelector("head").appendChild(style);
return style;
}
function setClass(element, className, enable) {
if (element.classList.contains(className) == enable)
return;
if (enable)
element.classList.add(className);
else
element.classList.remove(className);
}
function setDataSet(dataset, name, value) {
if (value)
dataset[name] = value;
else
delete dataset[name];
}
function getCSSValues(properties) {
let div = document.createElement("div");
let style = [];
for (let [key, value] of Object.entries(properties))
style += \`--\${key}:\${value};
\`;
div.style = style;
document.body.appendChild(div);
let computed = getComputedStyle(div);
let results = {};
for (let key of Object.keys(properties))
results[key] = computed.getPropertyValue(\`--\${key}\`);
div.remove();
return results;
}
function getSafeAreaInsets() {
let { left, top, right, bottom } = getCSSValues({
left: "env(safe-area-inset-left)",
top: "env(safe-area-inset-top)",
right: "env(safe-area-inset-right)",
bottom: "env(safe-area-inset-bottom)"
});
left = parseInt(left ?? 0);
top = parseInt(top ?? 0);
right = parseInt(right ?? 0);
bottom = parseInt(bottom ?? 0);
return { left, top, right, bottom };
}
function saveScrollPosition(scroller, saveRelativeTo) {
return {
originalScrollTop: scroller.scrollTop,
originalOffsetTop: saveRelativeTo.offsetTop
};
}
function restoreScrollPosition(scroller, restoreRelativeTo, savedPosition) {
let scrollTop = savedPosition.originalScrollTop;
if (restoreRelativeTo) {
let offset = restoreRelativeTo.offsetTop - savedPosition.originalOffsetTop;
scrollTop += offset;
}
if (scroller.scrollTop != scrollTop)
scroller.scrollTop = scrollTop;
}
function createFromTemplate(template, { makeSVGUnique = true } = {}) {
let node = document.importNode(template.content, true).firstElementChild;
if (makeSVGUnique) {
for (let svg of node.querySelectorAll("svg"))
makeSVGIdsUnique(svg);
}
return node;
}
var _svgIdSequence = 0;
function makeSVGIdsUnique(svg) {
let idMap = {};
let idx = _svgIdSequence;
for (let def of svg.querySelectorAll("[id]")) {
let oldId = def.id;
let newId = def.id + "_" + idx;
idx++;
idMap[oldId] = newId;
def.id = newId;
}
for (let node of svg.querySelectorAll("*")) {
for (let attr of node.getAttributeNames()) {
let value = node.getAttribute(attr);
let newValue = value;
if ((attr == "href" || attr == "xlink:href") && value.startsWith("#")) {
let oldId = value.substr(1);
let newId = idMap[oldId];
if (newId == null) {
console.warn("Unmatched SVG ID:", oldId);
continue;
}
newValue = "#" + newId;
}
let re = /url\\(#.*?\\)/;
newValue = newValue.replace(re, (str2) => {
let re2 = /url\\(#(.*)\\)/;
let oldId = str2.match(re2)[1];
let newId = idMap[oldId];
if (newId == null) {
console.warn("Unmatched SVG ID:", oldId);
return str2;
}
return "url(#" + newId + ")";
});
if (newValue != value)
node.setAttribute(attr, newValue);
}
}
_svgIdSequence = idx;
}
function setSizeAsProperty(node, { heightProperty, widthProperty, target, signal } = {}) {
if (target == null)
target = node;
let refreshSize = () => {
let { width, height } = node.getBoundingClientRect();
if (widthProperty)
target.style.setProperty(widthProperty, \`\${Math.floor(width)}px\`);
if (heightProperty)
target.style.setProperty(heightProperty, \`\${Math.floor(height)}px\`);
};
let resizeObserver = new ResizeObserver(() => refreshSize());
resizeObserver.observe(node);
if (signal)
signal.addEventListener("abort", () => resizeObserver.disconnect());
refreshSize();
}
function getRelativePosition(element, ancestor) {
let x = 0, y = 0;
while (element != null && element != ancestor) {
x += element.offsetLeft;
y += element.offsetTop;
let searchFor = element.offsetParent;
while (element != ancestor && element != searchFor)
element = element.parentNode;
}
return [x, y];
}
/\x2f web/vview/util/other.js
var other_exports = {};
__export(other_exports, {
addToBeginning: () => addToBeginning,
addToEnd: () => addToEnd,
arrayBufferToHex: () => arrayBufferToHex,
arrayEqual: () => arrayEqual,
awaitMap: () => awaitMap,
awaitWithTimeout: () => awaitWithTimeout,
blankImage: () => blankImage,
canvasToBlob: () => canvasToBlob,
createUuid: () => createUuid,
defer: () => defer,
findById: () => findById,
findFirstIdx: () => findFirstIdx,
findLastIdx: () => findLastIdx,
interval: () => interval,
isPhone: () => isPhone,
makeFunction: () => makeFunction,
makePromise: () => makePromise,
preloadImages: () => preloadImages,
shuffleArray: () => shuffleArray,
sleep: () => sleep,
sortQueryParameters: () => sortQueryParameters,
vsync: () => vsync,
waitForContentLoaded: () => waitForContentLoaded,
waitForEvent: () => waitForEvent,
waitForImageDimensions: () => waitForImageDimensions,
waitForImageLoad: () => waitForImageLoad,
waitForWebSocketMessage: () => waitForWebSocketMessage,
waitForWebSocketOpened: () => waitForWebSocketOpened,
xmlns: () => xmlns,
zip: () => zip
});
var blankImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=";
var xmlns = "http:/\x2fwww.w3.org/2000/svg";
function preloadImages(images) {
let preload = document.createElement("div");
for (let i = 0; i < images.length; ++i) {
let img = document.createElement("img");
img.src = images[i];
preload.appendChild(img);
}
}
function defer(func) {
return Promise.resolve().then(() => {
func();
});
}
function sleep(ms, { signal = null } = {}) {
return new Promise((accept, reject) => {
let timeout = null;
let abort = () => {
realClearTimeout(timeout);
reject("aborted");
};
if (signal != null)
signal.addEventListener("abort", abort, { once: true });
timeout = realSetTimeout(() => {
if (signal)
signal.removeEventListener("abort", abort, { once: true });
accept();
}, ms);
});
}
function makePromise() {
let accept, reject;
let promise = new Promise((a, r) => {
accept = a;
reject = r;
});
promise.accept = accept;
promise.reject = reject;
return promise;
}
function makeFunction(value) {
if (value instanceof Function)
return value;
else
return () => value;
}
async function awaitMap(map) {
Promise.all(Object.values(map));
let results = {};
for (let [key, promise] of Object.entries(map))
results[key] = await promise;
return results;
}
function interval(callback, ms, signal, callImmediately = true) {
if (signal && signal.aborted)
return;
let id = realSetInterval(callback, ms);
if (signal) {
signal.addEventListener("abort", () => {
realClearInterval(id);
}, { once: true });
}
if (callImmediately)
callback();
}
function waitForContentLoaded() {
return new Promise((accept, reject) => {
if (document.readyState != "loading") {
accept();
return;
}
window.addEventListener("DOMContentLoaded", (e) => {
accept();
}, {
capture: true,
once: true
});
});
}
function waitForEvent(node, name, { signal = null } = {}) {
return new Promise((resolve, reject) => {
if (signal && signal.aborted) {
resolve(null);
return;
}
let removeListenersSignal = new AbortController();
node.addEventListener(name, (e) => {
removeListenersSignal.abort();
resolve(e);
}, { signal: removeListenersSignal.signal });
if (signal) {
signal.addEventListener("abort", (e) => {
removeListenersSignal.abort();
resolve("aborted");
}, { signal: removeListenersSignal.signal });
}
});
}
function waitForImageLoad(img, signal) {
return new Promise((resolve, reject) => {
let src = img.src;
if (img.complete) {
resolve(null);
return;
}
if (signal && signal.aborted) {
img.src = blankImage;
resolve("aborted");
return;
}
let removeListenersSignal = new AbortController();
img.addEventListener("error", (e) => {
console.log("Error loading image:", src);
removeListenersSignal.abort();
resolve("failed");
}, { signal: removeListenersSignal.signal });
img.addEventListener("load", (e) => {
removeListenersSignal.abort();
resolve(null);
}, { signal: removeListenersSignal.signal });
if (signal) {
signal.addEventListener("abort", (e) => {
img.src = blankImage;
removeListenersSignal.abort();
resolve("aborted");
}, { signal: removeListenersSignal.signal });
}
});
}
async function waitForImageDimensions(img, signal) {
return new Promise((resolve, reject) => {
if (signal && signal.aborted)
resolve(false);
if (img.naturalWidth != 0)
resolve(true);
let frame_id = null;
let abort = () => {
signal.removeEventListener("aborted", abort);
if (frame_id != null)
realCancelAnimationFrame(frame_id);
resolve(false);
};
if (signal)
signal.addEventListener("aborted", abort);
let check = () => {
if (img.naturalWidth != 0) {
resolve(true);
if (signal)
signal.removeEventListener("aborted", abort);
return;
}
frame_id = realRequestAnimationFrame(check);
};
check();
});
}
function canvasToBlob(canvas, {
type = "image/jpeg",
quality = 1
}) {
return new Promise((resolve) => {
canvas.toBlob((blob) => resolve(blob), type, quality);
});
}
async function awaitWithTimeout(promise, ms) {
let sleep2 = new Promise((accept, reject) => {
realSetTimeout(() => {
accept("timed-out");
}, ms);
});
return await Promise.any([promise, sleep2]);
}
function vsync({ signal = null } = {}) {
return new Promise((accept, reject) => {
let id = null;
let abort = () => {
if (id != null)
realCancelAnimationFrame(id);
accept(false);
};
if (signal?.aborted) {
abort();
return;
}
id = realRequestAnimationFrame((time) => {
if (signal)
signal.removeEventListener("abort", abort);
accept(true);
});
if (signal)
signal.addEventListener("abort", abort, { once: true });
});
}
function waitForWebSocketOpened(websocket, { signal } = {}) {
return new Promise((resolve) => {
let cleanupAbort = new AbortController();
let onopen = (e) => {
cleanupAbort.abort();
resolve(true);
};
let onerror = (e) => {
cleanupAbort.abort();
resolve(false);
};
websocket.addEventListener("open", onopen, { signal: cleanupAbort.signal });
websocket.addEventListener("error", onerror, { signal: cleanupAbort.signal });
if (signal)
signal.addEventHandler("abort", () => onclose(), { signal: cleanupAbort.signal });
});
}
function waitForWebSocketMessage(websocket, { signal } = {}) {
return new Promise((resolve) => {
let cleanupAbort = new AbortController();
let onmessage = (e) => {
cleanupAbort.abort();
try {
let data = JSON.parse(e.data);
resolve(data);
} catch (e2) {
console.log("Invalid data received from socket:", websocket);
console.log(e2.data);
}
};
let onclose2 = (e) => {
cleanupAbort.abort();
resolve(null);
};
websocket.addEventListener("message", onmessage, { signal: cleanupAbort.signal });
websocket.addEventListener("close", onclose2, { signal: cleanupAbort.signal });
if (signal)
signal.addEventHandler("abort", () => onclose2(), { signal: cleanupAbort.signal });
});
}
function arrayEqual(lhs, rhs) {
if (lhs.length != rhs.length)
return false;
for (let idx = 0; idx < lhs.length; ++idx)
if (lhs[idx] != rhs[idx])
return false;
return true;
}
function findFirstIdx(A, B) {
for (let idx = 0; idx < A.length; ++idx) {
let idx2 = B.indexOf(A[idx]);
if (idx2 != -1)
return idx2;
}
return -1;
}
function findLastIdx(A, B) {
for (let idx = A.length - 1; idx >= 0; --idx) {
let idx2 = B.indexOf(A[idx]);
if (idx2 != -1)
return idx2;
}
return -1;
}
function createUuid() {
let data = new Uint8Array(32);
crypto.getRandomValues(data);
data[8] &= 63;
data[8] |= 128;
data[6] &= 15;
data[6] |= 4 << 4;
let result = "";
for (let i = 0; i < 4; ++i)
result += data[i].toString(16).padStart(2, "0");
result += "-";
for (let i = 4; i < 6; ++i)
result += data[i].toString(16).padStart(2, "0");
result += "-";
for (let i = 6; i < 8; ++i)
result += data[i].toString(16).padStart(2, "0");
result += "-";
for (let i = 8; i < 10; ++i)
result += data[i].toString(16).padStart(2, "0");
result += "-";
for (let i = 10; i < 16; ++i)
result += data[i].toString(16).padStart(2, "0");
return result;
}
function addToBeginning(object, key, value) {
let result = {};
result[key] = value;
for (let [oldKey, oldValue] of Object.entries(object)) {
if (oldKey != key)
result[oldKey] = oldValue;
}
return result;
}
function addToEnd(object, key, value) {
delete object[key];
object[key] = value;
}
function shuffleArray(array) {
for (let idx = 0; idx < array.length; ++idx) {
let swap_with = Math.floor(Math.random() * array.length);
[array[idx], array[swap_with]] = [array[swap_with], array[idx]];
}
}
function* zip(...args) {
let iters = [];
for (let arg of args)
iters.push(arg[Symbol.iterator]());
while (1) {
let values = [];
for (let iter of iters) {
let { value, done } = iter.next();
if (done)
return;
values.push(value);
}
yield values;
}
}
function isPhone() {
return Math.min(window.innerWidth, window.innerHeight) < 500;
}
function sortQueryParameters(search) {
let searchKeys = Array.from(search.keys());
searchKeys.sort();
let result = new URLSearchParams();
for (let key of searchKeys)
result.set(key, search.get(key));
return result;
}
function arrayBufferToHex(data) {
data = new Uint8Array(data);
let hashArray = Array.from(data);
let hash = hashArray.map((byte) => byte.toString(16).padStart(2, "0"));
return hash.join("");
}
function findById(array, name, id) {
for (let item of array) {
if (item[name] == id)
return item;
}
return null;
}
/\x2f web/vview/util/args.js
var Args = class {
constructor(url) {
if (url == null)
throw ValueError("url must not be null");
url = new URL(url, ppixiv.plocation);
this.path = url.pathname;
this.query = url.searchParams;
let { path: hashPath, query: hash_query } = Args.getHashArgs(url);
this.hash = hash_query;
this.hashPath = hashPath;
this.state = {};
}
static isPPixivUrl(url) {
if (ppixiv.native)
return new URL(url).origin == document.location.origin;
else
return url.hash.startsWith("#ppixiv");
}
static getHashArgs(url) {
if (!this.isPPixivUrl(url))
return { path: "", query: new URLSearchParams() };
let hashPath = url.hash;
if (hashPath.startsWith("#ppixiv"))
hashPath = hashPath.substr(7);
else if (hashPath.startsWith("#"))
hashPath = hashPath.substr(1);
let idx = hashPath.indexOf("?");
let query = null;
if (idx != -1) {
query = hashPath.substr(idx + 1);
hashPath = hashPath.substr(0, idx);
}
hashPath = hashPath.replace(/\\+/g, " ");
hashPath = decodeURIComponent(hashPath);
if (query == null)
return { path: hashPath, query: new URLSearchParams() };
else
return { path: hashPath, query: new URLSearchParams(query) };
}
static encodeURLPart(regex, part) {
return part.replace(regex, (c) => {
let ord = c.charCodeAt(0);
if (ord >= 128)
return c;
if (c == " ")
return "+";
let hex = ord.toString(16).padStart("0", 2);
return "%" + hex;
});
}
static encodeURLHash(hash) {
return Args.encodeURLPart(/[^A-Za-z0-9-_\\.!~\\*'()/:\\[\\]\\^#=&]/g, hash);
}
static encodeHashParam(param) {
return Args.encodeURLPart(/[^A-Za-z0-9-_\\.!~\\*'()/:\\[\\]\\^#]/g, param);
}
static encodeHashParams(params) {
let values = [];
for (let key of params.keys()) {
let key_values = params.getAll(key);
for (let value of key_values) {
key = Args.encodeHashParam(key);
value = Args.encodeHashParam(value);
values.push(key + "=" + value);
}
}
return values.join("&");
}
static get location() {
let result = new this(ppixiv.plocation);
result.state = JSON.parse(JSON.stringify(ppixiv.phistory.state)) || {};
return result;
}
get url() {
let url = new URL(ppixiv.plocation);
url.pathname = this.path;
url.search = this.query.toString();
url.hash = ppixiv.native ? "#" : "#ppixiv";
if (this.hashPath != "") {
if (!this.hashPath.startsWith("/"))
url.hash += "/";
url.hash += Args.encodeURLHash(this.hashPath);
}
let hash_string = Args.encodeHashParams(this.hash);
if (hash_string != "")
url.hash += "?" + hash_string;
return url;
}
toString() {
return this.url.toString();
}
get(key) {
let hash = key.startsWith("#");
let path = key.startsWith("/");
if (hash || path)
key = key.substr(1);
if (path)
return this.getPathnameSegment(parseInt(key));
let params = hash ? this.hash : this.query;
return params.get(key);
}
set(key, value) {
let hash = key.startsWith("#");
let path = key.startsWith("/");
if (hash || path)
key = key.substr(1);
if (path) {
this.setPathnameSegment(parseInt(key), value);
return;
}
let params = hash ? this.hash : this.query;
if (value != null)
params.set(key, value);
else
params.delete(key);
}
getPathnameSegment(idx) {
idx++;
let parts = this.path.split("/");
if (idx >= parts.length)
return null;
return decodeURIComponent(parts[idx]);
}
setPathnameSegment(idx, value) {
idx++;
let parts = this.path.split("/");
if (value != null) {
value = encodeURIComponent(value);
if (idx < parts.length)
parts[idx] = value;
else if (idx == parts.length)
parts.push(value);
else
console.warn(\`Can't set pathname segment \${idx} to \${value} past the end: \${this.toString()}\`);
} else {
if (idx == parts.length - 1)
parts.pop();
else if (idx < parts.length - 1)
console.warn(\`Can't remove pathname segment \${idx} in the middle: \${this.toString()}\`);
}
this.path = parts.join("/");
}
};
/\x2f web/vview/util/media-id.js
var media_id_exports = {};
__export(media_id_exports, {
_parseMediaIdInner: () => _parseMediaIdInner,
encodeMediaId: () => encodeMediaId,
fromIllustId: () => fromIllustId,
getMediaIdFirstPage: () => getMediaIdFirstPage,
getMediaIdForPage: () => getMediaIdForPage,
isLocal: () => isLocal,
parse: () => parse,
toIllustIdAndPage: () => toIllustIdAndPage
});
function encodeMediaId({ type, id, page = null } = {}) {
if (type == "illust") {
if (page == null)
page = 0;
id += "-" + page;
}
return type + ":" + id;
}
var _mediaIdCache = /* @__PURE__ */ new Map();
function parse(mediaId) {
let cache = _mediaIdCache.get(mediaId);
if (cache == null) {
cache = _parseMediaIdInner(mediaId);
_mediaIdCache.set(mediaId, cache);
}
return { type: cache.type, id: cache.id, page: cache.page };
}
function _parseMediaIdInner(mediaId) {
let { type, id } = _splitId(mediaId);
if (type != "illust")
return { type, id, page: 0 };
if (mediaId.indexOf("-") == -1)
return { type, id, page: 0 };
let parts = id.split("-");
let page = parts[1];
page = parseInt(page);
id = parts[0];
return { type, id, page };
}
function _splitId(id) {
if (id == null)
return {};
let parts = id.split(":");
let type = parts.length < 2 ? "illust" : parts[0];
let actual_id = parts.length < 2 ? id : parts.splice(1).join(":");
return {
type,
id: actual_id
};
}
function fromIllustId(illustId, page) {
if (illustId == null)
return null;
let { type, id } = _splitId(illustId);
if (type == "illust") {
id += "-";
id += page || 0;
} else {
console.assert(page == null || page == 0);
}
return type + ":" + id;
}
function toIllustIdAndPage(mediaId) {
let { type, id, page } = parse(mediaId);
if (type != "illust")
return [mediaId, 0];
return [id, page];
}
function isLocal(mediaId) {
let { type } = parse(mediaId);
return type == "file" || type == "folder";
}
function getMediaIdFirstPage(mediaId) {
return this.getMediaIdForPage(mediaId, 0);
}
function getMediaIdForPage(mediaId, page = 0) {
if (mediaId == null)
return null;
let id = parse(mediaId);
id.page = page;
return encodeMediaId(id);
}
/\x2f web/vview/util/pixiv.js
var pixiv_exports = {};
__export(pixiv_exports, {
adjustImageUrlHostname: () => adjustImageUrlHostname,
fixPixivLink: () => fixPixivLink,
fixPixivLinks: () => fixPixivLinks,
getHighResThumbnailUrl: () => getHighResThumbnailUrl,
getPageTypeFromUrl: () => getPageTypeFromUrl,
getPathWithoutLanguage: () => getPathWithoutLanguage,
getSearchTagsFromUrl: () => getSearchTagsFromUrl,
getTagSearchFromArgs: () => getTagSearchFromArgs,
getUrlWithoutLanguage: () => getUrlWithoutLanguage,
groupTagsByTranslation: () => groupTagsByTranslation,
makePixivLinksInternal: () => makePixivLinksInternal,
pixivImageHosts: () => pixivImageHosts,
splitSearchTags: () => splitSearchTags,
splitTagPrefixes: () => splitTagPrefixes,
tagsContainDot: () => tagsContainDot
});
function tagsContainDot(tagList) {
if (tagList == null)
return false;
for (let tag of tagList)
if (tag.indexOf("ドット") != -1)
return true;
return false;
}
function splitTagPrefixes(tag) {
if (tag[0] == "-")
return ["-", tag.substr(1)];
else
return ["", tag];
}
function groupTagsByTranslation(autocompletedTags, translatedTags) {
const tagGroupReducer = (acc, tag) => {
const strippedTag = tag.tag.replace(/\\s*\\(.+\\)\\s*/g, "");
if (!Object.hasOwn(translatedTags, strippedTag)) {
acc.standalone.push({ tag: tag.tag });
return acc;
}
const translatedTag = translatedTags[strippedTag] ?? tag.tag;
let slug = translatedTag.toLowerCase();
acc.groups[slug] ??= { tag: /* @__PURE__ */ new Set() };
acc.groups[slug].tag.add(tag.tag);
return acc;
};
let groupedTags = { groups: {}, standalone: [] };
groupedTags = autocompletedTags.reduce(tagGroupReducer, groupedTags);
let tagCounts = /* @__PURE__ */ new Map();
for (let tag of autocompletedTags)
tagCounts.set(tag.tag, parseInt(tag.accessCount));
let convertedGroups = [];
for (let [forTag, { tag }] of Object.entries(groupedTags.groups)) {
const tags = Array.from(tag).toSorted((lhs, rhs) => tagCounts.get(rhs) - tagCounts.get(lhs));
const search = tags.length === 1 ? tags[0] : \`( \${tags.join(" OR ")} )\`;
convertedGroups.push({
tag: search,
tagList: tags,
forTag
});
}
return [
...convertedGroups,
...groupedTags.standalone
];
}
function getPathWithoutLanguage(path) {
if (/^\\/..\\/\x2f.exec(path))
return path.substr(3);
else
return path;
}
function getUrlWithoutLanguage(url) {
url.pathname = getPathWithoutLanguage(url.pathname);
return url;
}
function splitSearchTags(search) {
search = search.replace(" ", " ");
search = search.replace(/ +/g, " ");
return search.split(" ");
}
function fixPixivLink(link) {
let url = new URL(link);
if (url.pathname != "/jump.php")
return link;
if (url.searchParams.has("url"))
return url.searchParams.get("url");
else {
let target = url.search.substr(1);
target = decodeURIComponent(target);
return target;
}
}
function fixPixivLinks(root) {
for (let a of root.querySelectorAll("A[target='_blank']"))
a.target = "";
for (let a of root.querySelectorAll("A")) {
if (a.relList == null)
a.rel += " noreferrer noopener";
else {
a.relList.add("noreferrer");
a.relList.add("noopener");
}
}
for (let a of root.querySelectorAll("A[href*='jump.php']"))
a.href = fixPixivLink(a.href);
}
function makePixivLinksInternal(root) {
for (let a of root.querySelectorAll("A")) {
let url = new URL(a.href, ppixiv.plocation);
if (url.hostname != "pixiv.net" && url.hostname != "www.pixiv.net" || url.hash != "")
continue;
url.hash = "#ppixiv";
a.href = url.toString();
}
}
function getSearchTagsFromUrl(url) {
url = getUrlWithoutLanguage(url);
let parts = url.pathname.split("/");
let tags = parts[2] || "";
return decodeURIComponent(tags);
}
function getPageTypeFromUrl(url) {
url = new URL(url);
url = getUrlWithoutLanguage(url);
let parts = url.pathname.split("/");
return parts[1];
}
function getTagSearchFromArgs(url) {
url = getUrlWithoutLanguage(url);
let type = getPageTypeFromUrl(url);
if (type != "tags")
return null;
let parts = url.pathname.split("/");
return decodeURIComponent(parts[2]);
}
var pixivImageHosts = Object.freeze({
pixiv: { name: "Pixiv", url: "i.pximg.net" }
});
var _allPixivImageHosts = /* @__PURE__ */ new Set();
for (let { url } of Object.values(pixivImageHosts)) {
_allPixivImageHosts.add(url);
}
function adjustImageUrlHostname(url, { host = null } = {}) {
url = new URL(url);
if (!_allPixivImageHosts.has(url.hostname)) {
console.error(url);
return url;
}
if (host == null)
host = ppixiv.settings.get("pixiv_cdn");
let hostname = pixivImageHosts[host]?.url ?? "i.pximg.net";
url.hostname = hostname;
return url;
}
function getHighResThumbnailUrl(url, page = 0) {
url = url.replace("/custom-thumb/", "/img-master/");
url = new URL(url, ppixiv.plocation);
let path = url.pathname;
let re = /(\\/c\\/)([^\\/]+)(.*)(square1200|master1200|custom1200).jpg/;
let match = re.exec(path);
if (match == null) {
console.warn("Couldn't parse thumbnail URL:", path);
return url.toString();
}
url.pathname = match[1] + "540x540_70" + match[3] + "master1200.jpg";
if (page != 0) {
url.pathname = url.pathname.replace("_p0_master1200", "_p" + page + "_master1200");
}
return url.toString();
}
/\x2f web/vview/util/pixiv-request.js
var pixiv_request_exports = {};
__export(pixiv_request_exports, {
fetchDocument: () => fetchDocument,
get: () => get,
post: () => post,
rpcPost: () => rpcPost,
sendPixivRequest: () => sendPixivRequest,
setPixivRequestInfo: () => setPixivRequestInfo
});
var requestInfo = {
csrfToken: null,
userId: null
};
function setPixivRequestInfo({ csrfToken, userId }) {
requestInfo.csrfToken = csrfToken;
requestInfo.userId = userId;
}
async function get(url, data, options) {
let params = createSearchParams(data);
let query = params.toString();
if (query != "")
url += "?" + query;
let result = await sendPixivRequest({
method: "GET",
url,
responseType: "json",
signal: options?.signal,
cache: options?.cache,
headers: {
Accept: "application/json"
}
});
if (result == null)
result = { error: true, message: "Invalid response" };
return result;
}
function createSearchParams(data) {
let params = new URLSearchParams();
for (let key in data) {
let value = data[key];
if (Array.isArray(value)) {
for (let item of value)
params.append(key, item);
} else
params.append(key, value);
}
return params;
}
async function post(url, data) {
let result = await sendPixivRequest({
"method": "POST",
"url": url,
"responseType": "json",
"data": JSON.stringify(data),
"headers": {
"Accept": "application/json",
"Content-Type": "application/json; charset=utf-8"
}
});
return result;
}
async function rpcPost(url, data) {
let result = await sendPixivRequest({
method: "POST",
url,
data: encodeQuery(data),
responseType: "json",
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
}
});
return result;
}
function encodeQuery(data) {
let str2 = [];
for (let key in data) {
if (!data.hasOwnProperty(key))
continue;
str2.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
}
return str2.join("&");
}
async function sendPixivRequest({ ...options }) {
options.headers ??= {};
let hostname = new URL(options.url, window.location).hostname;
if (hostname == "www.pixiv.net" && requestInfo.csrfToken) {
options.headers["x-csrf-token"] = requestInfo.csrfToken;
options.headers["x-user-id"] = requestInfo.userId;
}
let result = await sendRequest(options);
if (result == null)
return null;
if (options.responseType == "json") {
try {
return await result.json();
} catch (e) {
let message = \`\${result.status} \${result.statusText}\`;
console.log(\`Couldn't parse API result for \${options.url}: \${message}\`);
return { error: true, message };
}
}
if (options.responseType == "document") {
let text = await result.text();
return new DOMParser().parseFromString(text, "text/html");
}
return result;
}
async function sendRequest(options) {
if (options == null)
options = {};
let data = {};
data.method = options.method || "GET";
data.signal = options.signal;
data.cache = options.cache ?? "default";
if (options.data)
data.body = options.data;
if (options.headers) {
let headers = new Headers();
for (let key in options.headers)
headers.append(key, options.headers[key]);
data.headers = headers;
}
let fetch = window.realFetch ?? window.fetch;
try {
return await fetch(options.url, data);
} catch (e) {
if (data.signal && data.signal.aborted)
return null;
console.error("Error loading %s", options.url, e);
if (options.data)
console.error("Data:", options.data);
return null;
}
}
async function fetchDocument(url, headers = {}, options = {}) {
return await this.sendPixivRequest({
method: "GET",
url,
responseType: "document",
cache: options.cache,
headers,
...options
});
}
/\x2f web/vview/misc/helpers.js
var _helpers = class {
static getIconClassAndName(iconName) {
let [iconSet, name] = iconName.split(":");
if (name == null) {
name = iconSet;
iconSet = "mat";
}
let iconClass = "material-icons";
if (iconSet == "ppixiv")
iconClass = "ppixiv-icon";
else if (iconSet == "mat")
iconClass = "material-icons";
return [iconClass, name];
}
static createIcon(iconName, { asElement = false, classes = [], align = null, dataset = {} } = {}) {
let [iconClass, name] = _helpers.getIconClassAndName(iconName);
let icon = document.createElement("span");
icon.classList.add("font-icon");
icon.classList.add(iconClass);
icon.setAttribute("translate", "no");
icon.lang = "icon";
icon.innerText = name;
for (let className of classes)
icon.classList.add(className);
if (align != null)
icon.style.verticalAlign = align;
for (let [key, value] of Object.entries(dataset))
icon.dataset[key] = value;
if (asElement)
return icon;
else
return icon.outerHTML;
}
static replaceInlines(root) {
for (let element of root.querySelectorAll("img")) {
let src = element.getAttribute("src");
if (!src || !src.startsWith("ppixiv:"))
continue;
let name = src.substr(7);
let resource = ppixiv.resources[name];
if (resource == null) {
console.warn('Unknown resource "' + name + '" in', element);
resource = blankImage;
}
element.setAttribute("src", resource);
element.dataset.originalUrl = src;
}
for (let element of root.querySelectorAll("ppixiv-inline")) {
let src = element.getAttribute("src");
let node = _helpers.createInlineIcon(src);
element.replaceWith(node);
for (let attr of element.attributes) {
if (attr.name === "src")
continue;
if (node.hasAttribute(attr.name)) {
console.warn("Node", node, "already has attribute", attr);
continue;
}
node.setAttribute(attr.name, attr.value);
}
}
}
static createBoxLink({
label,
link = null,
classes = "",
icon = null,
popup = null,
explanation = null,
asElement = false,
dataset = {},
dataType = null
}) {
if (!this._cachedBoxLinkTemplate) {
let html = \`
<a class=box-link>
<div class=label-box>
<span hidden class=icon></span>
<span hidden class=label></span>
<vv-container class=widget-box></vv-container>
</div>
<span hidden class=explanation></span>
</a>
\`;
this._cachedBoxLinkTemplate = document.createElement("template");
this._cachedBoxLinkTemplate.innerHTML = html;
}
let node = _helpers.html.createFromTemplate(this._cachedBoxLinkTemplate);
if (label != null) {
node.querySelector(".label").hidden = false;
node.querySelector(".label").innerText = label;
}
if (link)
node.href = link;
for (let className of classes || []) {
if (className.length)
node.classList.add(className);
}
if (popup) {
node.classList.add("popup");
node.dataset.popup = popup;
}
if (icon != null) {
let [iconClass, iconName] = _helpers.getIconClassAndName(icon);
let iconElement = node.querySelector(".icon");
iconElement.classList.add(iconClass);
iconElement.classList.add("font-icon");
iconElement.hidden = false;
iconElement.innerText = iconName;
iconElement.lang = "icon";
if (label != null)
iconElement.classList.add("with-text");
}
if (explanation != null) {
let explanation_node = node.querySelector(".explanation");
explanation_node.hidden = false;
explanation_node.innerText = explanation;
}
if (dataType != null)
node.dataset.type = dataType;
for (let [key, value] of Object.entries(dataset))
node.dataset[key] = value;
if (asElement)
return node;
else
return node.outerHTML;
}
static createInlineIcon(src) {
if (!this._resource_cache[src]) {
let resource = ppixiv.resources[src];
if (resource == null) {
console.error(\`Unknown resource \${src}\`);
return null;
}
let div = document.createElement("div");
div.innerHTML = resource;
let node2 = div.firstElementChild;
node2.remove();
node2.dataset.ppixivResource = src;
this._resource_cache[src] = node2;
}
let node = this._resource_cache[src];
return document.importNode(node, true);
}
static saveBlob(blob, filename) {
let blobUrl = URL.createObjectURL(blob);
let a = document.createElement("a");
a.hidden = true;
a.href = blobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
realSetTimeout(() => {
window.URL.revokeObjectURL(blobUrl);
a.remove();
}, 1e3);
}
static watchEdits(input, { signal } = {}) {
let dragging = false;
let insideEdit = false;
input.addEventListener(
"mousedown",
(e) => {
if (e.button != 0 || dragging)
return;
dragging = true;
},
{ signal }
);
input.addEventListener(
"mouseup",
(e) => {
if (e.button != 0 || !dragging)
return;
dragging = false;
if (insideEdit) {
insideEdit = false;
input.dispatchEvent(new Event("editend"));
}
},
{ signal }
);
input.addEventListener(
"input",
(e) => {
let send_editend = false;
if (!insideEdit) {
insideEdit = true;
input.dispatchEvent(new Event("editbegin"));
send_editend = !dragging;
}
input.dispatchEvent(new Event("edit"));
if (send_editend) {
insideEdit = false;
input.dispatchEvent(new Event("editend"));
}
},
{ signal }
);
}
static forceTargetBlankOnElement(node) {
if (node.href == "" || node.getAttribute("target") == "_blank")
return;
let url;
try {
url = new URL(node.href);
if (url.origin == document.location.origin)
return;
} catch (e) {
return;
}
node.setAttribute("target", "_blank");
}
static forceTargetBlank() {
if (!ppixiv.mobile)
return;
function updateNode(node) {
if (node.querySelectorAll == null)
return;
_helpers.forceTargetBlankOnElement(node);
for (let a of node.querySelectorAll("A:not([target='_blank'])"))
_helpers.forceTargetBlankOnElement(a);
}
updateNode(document.documentElement);
let observer = new MutationObserver((mutations) => {
for (let mutation of mutations) {
for (let node of mutation.addedNodes)
updateNode(node);
}
});
observer.observe(document.documentElement, {
subtree: true,
childList: true
});
}
static shouldIgnoreHorizontalDrag(event) {
if (!ppixiv.ios || window.history.length <= 1)
return false;
let width = 25;
return event.clientX < width || event.clientX > window.innerWidth - width;
}
static async hideBodyDuringRequest(func) {
document.body.style.opacity = 0;
let waitPromise = null;
try {
let delay = 50;
let start = Date.now();
while (Date.now() - start < delay)
await _helpers.other.vsync();
waitPromise = func();
start = Date.now();
while (Date.now() - start < delay)
await _helpers.other.vsync();
} finally {
document.body.style.opacity = 1;
}
await waitPromise;
}
static isFullscreen() {
let vvbrowser = this._vvbrowser();
if (vvbrowser)
return vvbrowser.getFullscreen();
if (document.fullscreenElement != null)
return true;
let windowWidth = window.innerWidth * devicePixelRatio;
let windowHeight = window.innerHeight * devicePixelRatio;
if (Math.abs(windowWidth - window.screen.width) < 2 && Math.abs(windowHeight - window.screen.height) < 2)
return true;
if (window.outerWidth == window.screen.width && window.outerHeight == window.screen.height)
return true;
return false;
}
static _vvbrowser({ sync = true } = {}) {
if (sync)
return window.chrome?.webview?.hostObjects?.sync?.vvbrowser;
else
return window.chrome?.webview?.hostObjects?.vvbrowser;
}
static async toggleFullscreen() {
await _helpers.hideBodyDuringRequest(async () => {
let vvbrowser = this._vvbrowser();
if (vvbrowser) {
vvbrowser.setFullscreen(!_helpers.isFullscreen());
return;
}
if (_helpers.isFullscreen())
document.exitFullscreen();
else
document.documentElement.requestFullscreen();
});
}
static areUrlsEquivalent(url1, url2) {
if (url1 == null || url2 == null)
return false;
url1 = _helpers.pixiv.getUrlWithoutLanguage(url1);
url2 = _helpers.pixiv.getUrlWithoutLanguage(url2);
return url1.toString() == url2.toString();
}
static setPageTitle(title) {
let title_element = document.querySelector("title");
if (title_element.textContent == title)
return;
let new_title = document.createElement("title");
new_title.textContent = title;
document.head.appendChild(new_title);
title_element.remove();
document.dispatchEvent(new Event("windowtitlechanged"));
}
static setPageIcon(url) {
document.querySelector("link[rel='icon']").href = url;
}
static getArgsForTagSearch(tags, url) {
url = _helpers.pixiv.getUrlWithoutLanguage(url);
let type = _helpers.pixiv.getPageTypeFromUrl(url);
if (type == "tags") {
let parts = url.pathname.split("/");
parts[2] = encodeURIComponent(tags);
url.pathname = parts.join("/");
} else {
url = new URL(
"/tags/" + encodeURIComponent(tags) + "/artworks#ppixiv",
url
);
}
let args = _helpers.getCanonicalUrl(url);
return args;
}
static getCanonicalUrl(url, {
startAtBeginning = true
} = {}) {
url = new URL(url);
url = _helpers.pixiv.getUrlWithoutLanguage(url);
let args = new _helpers.args(url);
if (startAtBeginning)
args.query.delete("p");
args.hash.delete("page");
args.hash.delete("view");
if (startAtBeginning)
args.hash.delete("illust_id");
args.hash.delete("virtual");
args.hash.delete("temp-view");
args.hash.delete("view-muted");
if (startAtBeginning)
args.hash.delete("file");
args.hash.delete("slideshow");
args.query = _helpers.other.sortQueryParameters(args.query);
args.hash = _helpers.other.sortQueryParameters(args.hash);
return args;
}
static inputHandler(input, submit) {
input.addEventListener("keydown", function(e) {
e.stopPropagation();
if (e.key == "Enter")
submit(e);
});
}
static navigate(args, {
addToHistory = true,
cause = "navigation",
scrollToTop = false,
sendPopstate = true
} = {}) {
if (args instanceof URL)
args = new _helpers.args(args);
let old_url = new _helpers.args(ppixiv.plocation).toString();
let history_data = {
...args.state
};
if (args.url.toString() == old_url && JSON.stringify(history_data) == JSON.stringify(history.state))
return;
if (addToHistory)
ppixiv.phistory.pushState(history_data, "", args.url.toString());
else
ppixiv.phistory.replaceState(history_data, "", args.url.toString());
if (ppixiv.plocation.toString() != old_url) {
if (sendPopstate) {
let event = new PopStateEvent("pp:popstate");
event.navigationCause = cause;
event.scrollToTop = scrollToTop;
window.dispatchEvent(event);
}
window.dispatchEvent(new PopStateEvent("pp:statechange"));
}
}
static getTitleForIllust(mediaInfo) {
if (mediaInfo == null)
return null;
let pageTitle = "";
if (!_helpers.mediaId.isLocal(mediaInfo.mediaId)) {
if (mediaInfo.bookmarkData)
pageTitle += "★";
pageTitle += mediaInfo.userName + " - " + mediaInfo.illustTitle;
return pageTitle;
} else {
let { id } = _helpers.mediaId.parse(mediaInfo.mediaId);
let name = _helpers.strings.getPathSuffix(id, 1, 0);
let parent = _helpers.strings.getPathSuffix(id, 2, 1);
pageTitle += \`\${name} - \${parent}\`;
}
return pageTitle;
}
static setTitle(mediaInfo) {
let pageTitle = _helpers.getTitleForIllust(mediaInfo) ?? "Loading...";
_helpers.setPageTitle(pageTitle);
}
static setIcon({ vview = false } = {}) {
if (ppixiv.native || vview)
_helpers.setPageIcon(ppixiv.resources["resources/vview-icon.png"]);
else
_helpers.setPageIcon(ppixiv.resources["resources/regular-pixiv-icon.png"]);
}
static setTitleAndIcon(mediaInfo) {
_helpers.setTitle(mediaInfo);
_helpers.setIcon();
}
static isZoomHotkey(e) {
if (!e.ctrlKey)
return null;
if (e.code == "NumpadAdd" || e.code == "Equal")
return 1;
if (e.code == "NumpadSubtract" || e.code == "Minus")
return -1;
return null;
}
};
var helpers = _helpers;
__publicField(helpers, "_resource_cache", {});
var Timer = class {
constructor(func) {
this.func = func;
}
_runFunc = () => {
this.func();
};
clear() {
if (this.id == null)
return;
realClearTimeout(this.id);
this.id = null;
}
set(ms) {
this.clear();
this.id = realSetTimeout(this._runFunc, ms);
}
};
var PointerEventMovement = class {
constructor() {
if ("movementX" in new PointerEvent("test"))
return;
this.last_pointer_positions = {};
window.addEventListener("pointerdown", (e) => this.pointerdown(e), {
capture: true
});
window.addEventListener("pointermove", (e) => this.pointerdown(e), {
capture: true
});
window.addEventListener("pointerup", (e) => this.pointerup(e), {
capture: true
});
window.addEventListener("pointercancel", (e) => this.pointerup(e), {
capture: true
});
}
pointerdown(e) {
let previousX = this.last_pointer_positions[e.pointerId]?.x ?? e.screenX;
let previousY = this.last_pointer_positions[e.pointerId]?.y ?? e.screenY;
this.last_pointer_positions[e.pointerId] = { x: e.screenX, y: e.screenY };
e.movementX = e.screenX - previousX;
e.movementY = e.screenY - previousY;
}
pointerup(e) {
delete this.last_pointer_positions[e.pointerId];
e.movementX = e.movementY = 0;
}
};
var GlobalKeyListener = class {
constructor() {
this.keys_pressed = /* @__PURE__ */ new Set();
this.listeners = /* @__PURE__ */ new Map();
window.addEventListener("keydown", (e) => {
if (this.keys_pressed.has(e.key))
return;
this.keys_pressed.add(e.key);
this._callListenersForKey(e.key, true);
});
window.addEventListener(
"keyup",
(e) => {
if (!this.keys_pressed.has(e.key))
return;
this.keys_pressed.delete(e.key);
this._callListenersForKey(e.key, false);
},
true
);
window.addEventListener("blur", (e) => {
this.releaseAllKeys();
});
window.addEventListener("contextmenu", async (e) => {
await helpers.other.sleep(0);
if (e.defaultPrevented)
return;
this.releaseAllKeys();
});
}
releaseAllKeys() {
for (let key of this.keys_pressed)
this._callListenersForKey(key, false);
this.keys_pressed.clear();
}
_getListenersForKey(key, { create = false } = {}) {
if (!this.listeners.has(key)) {
if (!create)
return [];
this.listeners.set(key, /* @__PURE__ */ new Set());
}
return this.listeners.get(key);
}
_registerListener(key, listener) {
let listeners_for_key = this._getListenersForKey(key, { create: true });
listeners_for_key.add(listener);
realSetTimeout(() => {
if (!this._getListenersForKey(key).has(listener))
return;
if (this.keys_pressed.has(key))
listener.keyChanged(true);
}, 0);
}
_unregisterListener(key, listener) {
let listeners_for_key = this._getListenersForKey(key, { create: false });
if (listeners_for_key)
listeners_for_key.delete(listener);
}
_callListenersForKey(key, down) {
let listeners_for_key = this._getListenersForKey(key, { create: false });
if (listeners_for_key == null)
return;
for (let key_listener of listeners_for_key.values())
key_listener.keyChanged(down);
}
};
var _KeyListener = class {
constructor(key, callback, { signal = null } = {}) {
if (_KeyListener.singleton == null)
_KeyListener.singleton = new GlobalKeyListener();
this.callback = callback;
this.pressed = false;
_KeyListener.singleton._registerListener(key, this);
if (signal) {
signal.addEventListener("abort", (e) => {
_KeyListener.singleton._unregisterListener(key, this);
});
}
}
keyChanged = (pressed) => {
if (this.pressed == pressed)
return;
this.pressed = pressed;
this.callback(pressed);
};
};
var KeyListener = _KeyListener;
__publicField(KeyListener, "singleton", null);
var GuardedRunner = class {
constructor({ signal } = {}) {
this._abort = null;
this._promise = null;
if (signal)
signal.addEventListener("abort", () => this.abort());
}
call(func, { ...args }) {
if (this._abort)
this.abort();
let abort = this._abort = new AbortController();
args = { ...args, signal: abort.signal };
let promise = this._promise = this._runIgnoringAborts(func, args);
promise.finally(() => {
if (this._abort == abort)
this._abort = null;
if (this._promise == promise)
this._promise = null;
});
return promise;
}
get promise() {
return this._promise;
}
get isRunning() {
return this._abort != null;
}
async _runIgnoringAborts(func, args) {
try {
return await func(args);
} catch (e) {
if (e.name == "AbortError")
return;
throw e;
}
}
abort() {
if (this._abort) {
this._abort.abort();
this._abort = null;
this._promise = null;
}
}
};
var FixedDOMRect = class extends DOMRect {
constructor(left, top, right, bottom) {
super(left, top, right - left, bottom - top);
}
get x1() {
return this.x;
}
get y1() {
return this.y;
}
get x2() {
return this.x + this.width;
}
get y2() {
return this.y + this.height;
}
set x1(value) {
this.width += this.x - value;
this.x = value;
}
set y1(value) {
this.height += this.y - value;
this.y = value;
}
set x2(value) {
this.width = value - super.x;
}
set y2(value) {
this.height = value - super.y;
}
get middleHorizontal() {
return (super.right + super.left) / 2;
}
get middleVertical() {
return (super.top + super.bottom) / 2;
}
extendOutwards(value) {
return new FixedDOMRect(
this.left - value,
this.top - value,
this.right + value,
this.bottom + value
);
}
cropTo(outer) {
return new FixedDOMRect(
helpers.math.clamp(this.x1, outer.x1, outer.x2),
helpers.math.clamp(this.y1, outer.y1, outer.y2),
helpers.math.clamp(this.x2, outer.x1, outer.x2),
helpers.math.clamp(this.y2, outer.y1, outer.y2)
);
}
};
AbortSignal.prototype.wait = function() {
if (this.aborted)
return;
if (this._promise == null) {
this._promise = new Promise((accept) => {
this._promise_accept = accept;
});
this.addEventListener(
"abort",
(e) => {
this._promise_accept();
},
{ once: true }
);
}
return this._promise;
};
var SafetyBackoffTimer = class {
constructor({
resetAfter = 60,
maxBackoff = 30,
exponent = 1.5
} = {}) {
this.resetAfterMs = resetAfter * 1e3;
this.maxBackoffTime = maxBackoff * 1e3;
this.exponent = exponent;
this.reset();
}
reset() {
this.reset_at = Date.now() + this.resetAfterMs;
this.backoff_count = 0;
}
async wait() {
if (Date.now() >= this.reset_at)
this.reset();
this.reset_at = Date.now() + this.resetAfterMs;
this.backoff_count++;
let delay_ms = Math.pow(this.exponent, this.backoff_count) * 1e3;
delay_ms = Math.min(delay_ms, this.maxBackoffTime);
console.log("wait for", delay_ms);
await helpers.other.sleep(delay_ms);
}
};
var ClassFlags = class extends EventTarget {
static get get() {
if (this.singleton == null)
this.singleton = new ClassFlags(document.documentElement);
return this.singleton;
}
constructor(element) {
super();
this.element = element;
let observer = new MutationObserver((mutations) => {
let mutation = mutations[0];
let old_classes = mutation.oldValue ?? "";
let old_set = new Set(old_classes.split(" "));
let new_set = this.element.classList;
for (let name of new_set)
if (!old_set.has(name))
this.broadcast(name, true);
for (let name of old_set)
if (!new_set.contains(name))
this.broadcast(name, false);
});
observer.observe(element, {
attributeFilter: ["class"],
attributeOldValue: true
});
}
get(name) {
return this.element.classList.contains(name);
}
set(name, value) {
helpers.html.setClass(this.element, name, value);
return true;
}
broadcast(name, value) {
let e = new Event(name);
e.value = value;
this.dispatchEvent(e);
}
};
var WakeupEvent = class {
constructor() {
this._signal = new AbortController();
}
async wait() {
await this._signal.signal.wait();
}
wake() {
this._signal.abort();
this._signal = new AbortController();
}
};
var OpenWidgets = class extends EventTarget {
static get singleton() {
if (this._singleton == null)
this._singleton = new this();
return this._singleton;
}
constructor() {
super();
this._openWidgets = /* @__PURE__ */ new Set();
this.event = new WakeupEvent();
}
get empty() {
return this._openWidgets.size == 0;
}
set(widget, value) {
if (value)
this.add(widget);
else
this.remove(widget);
}
_broadcastChanged() {
this.dispatchEvent(new Event("changed"));
}
add(widget) {
let wasEmpty = this.empty;
this._openWidgets.add(widget);
if (wasEmpty)
this._broadcastChanged();
}
async remove(widget) {
if (!this._openWidgets.has(widget))
return;
this._openWidgets.delete(widget);
if (this.event.size > 0)
return;
await null;
this._broadcastChanged();
if (this.event.size > 0)
return;
this.event.wake();
}
async waitUntilEmpty() {
while (!this.empty)
await this.event.wait();
}
get_all() {
return this._openWidgets;
}
};
helpers.math = math_exports;
helpers.strings = strings_exports;
helpers.html = html_exports;
helpers.other = other_exports;
helpers.args = Args;
helpers.mediaId = media_id_exports;
helpers.pixiv = pixiv_exports;
helpers.pixivRequest = pixiv_request_exports;
/\x2f web/vview/actors/actor.js
var templatesCache = /* @__PURE__ */ new Map();
var _Actor = class extends EventTarget {
static dumpActors({ parent = null } = {}) {
let actors = parent ? parent.children : _Actor._topActors;
let grouped = false;
if (parent) {
if (actors.length == 0)
console.log(parent);
else {
console.group(parent);
grouped = true;
}
}
try {
for (let actor of actors)
_Actor.dumpActors({ parent: actor });
} finally {
if (grouped)
console.groupEnd();
}
}
constructor({
parent = null,
signal = null,
...options
} = {}) {
super();
this.options = options;
this.parent = parent;
this.children = [];
this._shutdownSignalController = new AbortController();
this.shutdownSignal = this._shutdownSignalController.signal;
if (signal == null && this.parent != null)
signal = this.parent.shutdownSignal;
if (signal)
signal.addEventListener("abort", () => this.shutdown(), { once: true, ...this._signal });
if (this.parent)
this.parent._childAdded(this);
else
_Actor._topActors.push(this);
}
get className() {
return this.__proto__.constructor.name;
}
get hasShutdown() {
return this.shutdownSignal.aborted;
}
shutdown() {
if (_Actor._debugShutdown && !this._previousShutdownStack) {
try {
throw new Error();
} catch (e) {
this._previousShutdownStack = e.stack;
}
}
if (this.hasShutdown) {
console.error("Actor has already shut down:", this);
if (this._previousShutdownStack)
console.log("Previous shutdown stack:", this._previousShutdownStack);
return;
}
this._shutdownSignalController.abort();
if (this.children.length != 0) {
for (let child of this.children)
console.warn("Child of", this, "didn't shut down:", child);
}
if (this.parent)
this.parent._childRemoved(this);
else {
let idx = _Actor._topActors.indexOf(this);
console.assert(idx != -1);
_Actor._topActors.splice(idx, 1);
}
}
createTemplate({ name = null, html, makeSVGUnique = true }) {
let template = name ? this._templatesCache[name] : null;
if (!template) {
template = document.createElement("template");
template.innerHTML = html;
helpers.replaceInlines(template.content);
if (name)
this._templatesCache[name] = template;
}
return helpers.html.createFromTemplate(template, { makeSVGUnique });
}
get _templatesCache() {
let cache = templatesCache.get(this.constructor);
if (cache != null)
return cache;
cache = {};
templatesCache.set(this.constructor, cache);
return cache;
}
get _signal() {
return { signal: this.shutdownSignal };
}
_childAdded(child) {
this.children.push(child);
}
_childRemoved(child) {
let idx = this.children.indexOf(child);
if (idx == -1) {
console.warn("Actor wasn't in the child list:", child);
return;
}
this.children.splice(idx, 1);
}
*ancestors({ includeSelf = false } = {}) {
if (includeSelf)
yield this;
let count = 0;
let parent = this.parent;
while (parent != null) {
yield parent;
parent = parent.parent;
count++;
if (count > 1e4)
throw new Error("Recursion detected");
}
}
*descendents({ includeSelf = false } = {}) {
if (includeSelf)
yield this;
for (let child of this.children) {
yield child;
for (let childDescendants of child.descendents())
yield childDescendants;
}
}
isAncestorOf(widget) {
for (let ancestor of widget.ancestors({ includeSelf: true }))
if (ancestor == this)
return true;
return false;
}
getRoots() {
let result = [];
for (let child of this.children)
result = [...result, ...child.getRoots()];
return result;
}
get visible() {
return true;
}
get actuallyVisible() {
return true;
}
get visibleRecursively() {
if (!this.visible)
return false;
if (this.parent == null)
return true;
return this.parent.visibleRecursively;
}
get actuallyVisibleRecursively() {
if (!this.actuallyVisible)
return false;
if (this.parent == null)
return true;
return this.parent.actuallyVisibleRecursively;
}
callVisibilityChanged() {
for (let actor of this.descendents({ includeSelf: true })) {
actor.visibilityChanged();
}
}
visibilityChanged() {
}
};
var Actor = _Actor;
__publicField(Actor, "_debugShutdown", false);
__publicField(Actor, "_topActors", []);
/\x2f web/vview/widgets/widget.js
var Widget = class extends Actor {
static fromNode(node, { allowNone = false } = {}) {
if (node == null && allowNone)
return null;
let widgetTopNode = node.closest(".widget");
if (widgetTopNode == null) {
if (allowNone)
return null;
console.log("Node wasn't in a widget:", node);
throw new Error("Node wasn't in a widget:", node);
}
console.assert(widgetTopNode.widget != null);
return widgetTopNode.widget;
}
constructor({
container,
template = null,
visible = true,
parent = null,
containerPosition = "beforeend",
...options
} = {}) {
if (container != null && container instanceof Widget)
container = container.root;
if (parent == null) {
const parentSearchNode = container;
if (parentSearchNode == null && parent == null)
console.warn("Can't search for parent");
if (parentSearchNode) {
const parentWidget = Widget.fromNode(parentSearchNode, {
allowNone: true
});
if (parent != null && parent !== parentWidget) {
console.assert(parent === parentWidget);
console.log("Found:", parentWidget);
console.log("Expected:", parent);
}
parent = parentWidget;
}
}
super({ container, parent, ...options });
this.root = this.createTemplate({ html: template });
if (container != null) {
if (containerPosition === "replace")
container.replaceWith(this.root);
else
container.insertAdjacentElement(containerPosition, this.root);
}
this.root.classList.add("widget");
this.root.dataset.widget = this.className;
this.root.widget = this;
this._visible = visible;
helpers.other.defer(() => {
if (this.hasShutdown)
return;
this.afterInit();
});
}
afterInit() {
this.applyVisibility();
this.visibilityChanged();
this.refresh();
}
get container() {
console.warn("Deprecated widget.container");
return this.root;
}
async refresh() {
}
get visible() {
return this._visible;
}
set visible(value) {
if (value == this.visible)
return;
this._visible = value;
this.callVisibilityChanged();
}
get actuallyVisible() {
return this.visible;
}
visibilityChanged() {
super.visibilityChanged();
this.applyVisibility();
}
shutdown() {
super.shutdown();
this.root.remove();
}
applyVisibility() {
helpers.html.setClass(this.root, "hidden-widget", !this._visible);
}
visibilityChanged() {
super.visibilityChanged();
this.applyVisibility();
if (this.actuallyVisible) {
if (this.visibilityAbort == null)
this.visibilityAbort = new AbortController();
} else {
if (this.visibilityAbort)
this.visibilityAbort.abort();
this.visibilityAbort = null;
}
}
querySelector(selector) {
return this.root.querySelector(selector);
}
querySelectorAll(selector) {
return this.root.querySelectorAll(selector);
}
closest(selector) {
return this.root.closest(selector);
}
getRoots() {
let result = [this.root];
for (let widget of this.descendents()) {
if (widget.root == null)
continue;
if (helpers.html.isAbove(widget.parent.root, widget.root))
result.push(widget.root);
}
return result;
}
};
/\x2f web/vview/widgets/whats-new.js
var updateHistory = [
{
version: 245,
boring: true,
text: \`
Added an option to hide the AI-generated image icon.
\`
},
{
version: 236,
text: \`
Added a manga progress bar at the bottom of the image view (disabled by
default). This can be enabled in settings under "Image Viewing".
\`
},
{
version: 229,
text: \`
Select "Translate" from the context menu while viewing an image to enable translation
using <a href=https:/\x2fcotrans.touhou.ai/>Cotrans</a> (experimental).
<p>
Added support for viewing Pixiv image series.
\`
},
{
version: 218,
text: \`
Added support for the "Hide AI works" filter on searches.
\`
},
{
version: 210,
text: \`
Aspect ratio thumbnails are now used by default. Square thumbs can be selected in
settings.
\`
},
{
version: 198,
text: \`
Artist links have been moved to the avatar dropdown, and can be accessed directly
from the popup menu.
<p>
"AI-generated" is now displayed as a tag.
\`
},
{
version: 172,
text: \`
Added support for AI rankings.
\`
},
{
version: 168,
text: \`
Images tagged as "AI" are now marked in search results. There are too many of
these flooding the site, but this gives an alternative to muting them.
<p>
Slideshows are now limited to 60 FPS by default. <span class=explanation-button data-explanation=chrome-fps>(Why?)</span>
This can be disabled in settings.
<div class="explanation-target chrome-fps" hidden>
Chrome has problems with high refresh rate monitors, and can cause other windows to
stutter when animations are running on another monitor. Slideshows are usually
gradual pans anyway, so limiting the framerate avoids this problem without affecting
the result very much.
</div>
\`
},
{
version: 164,
boring: true,
text: \`
Search autocomplete now works for searches with multiple tags.
\`
},
{
version: 162,
text: \`
Search tags can now be saved in the search dropdown separately from recents and grouped
together.
<p>
Added "Loop" in the more options dropdown to loop the current image.
\`
},
{
version: 153,
boring: true,
text: \`
Pressing Ctrl-S now saves the current image or video, and Ctrl-Alt-S saves a
ZIP of the current manga post.
<p>
Fixed hotkeys in search results, so hotkeys like Ctrl-B work when hovering over
thumbnails.
\`
},
{
version: 152,
boring: true,
text: \`
Tags that have been searched for recently now appear at the top of the artist
tag list.
\`
},
{
version: 151,
boring: true,
text: \`
Navigating through images with the mousewheel now skips past manga pages if the image
is muted.
\`
},
{
version: 145,
text: \`
Added support for viewing followed users who are accepting requests, and a link from
the user to the request page. This feature is still being rolled out by Pixiv and may
not be available for all users immediately.
\`
},
{
version: 142,
boring: true,
text: \`
The slideshow can now be set to fade through images without panning.
<p>
Thumbnail panning now stops after a while if there's no mouse movement,
so it doesn't keep going forever.
\`
},
{
version: 139,
text: \`
Added a panning/slideshow editor, to edit how an image will pan and zoom during
slideshows. Right-click and enable
\${helpers.createIcon("settings")} \${helpers.createIcon("brush")} Image Editing, then
\${helpers.createIcon("wallpaper")} Edit Panning while viewing an image.
<p>
Added a button to \${helpers.createIcon("restart_alt")}
Refresh the search from the current page. The \${helpers.createIcon("refresh")}
Refresh button now always restarts from the beginning.
\`
},
{
version: 133,
text: \`
Pressing Ctrl-P now toggles image panning.
<p>
Added image cropping for trimming borders from images.
Enable \${helpers.createIcon("settings")} Image Editing in the context menu to
display the editor.
<p>
The page number is now shown over expanded manga posts while hovering over
the image, so you can collapse long posts without having to scroll back up.
\`
},
{
version: 132,
text: \`
Improved following users, allowing changing a follow to public or private and
adding support for follow tags.
\`
},
{
version: 129,
text: \`
Added a new way of viewing manga posts.
<p>
You can now view manga posts in search results. Click the page count in the corner of
thumbnails to show all manga pages. You can also click \${helpers.createIcon("open_in_full")}
in the top menu to expand everything, or turn it on everywhere in settings.
\`
},
{
version: 126,
text: \`
Muted tags and users can now be edited from the preferences menu.
<p>
Any number of tags can be muted. If you don't have Premium, mutes will be
saved to the browser instead of to your Pixiv account.
\`
},
{
version: 123,
text: \`
Added support for viewing completed requests.
<p>
Disabled light mode for now. It's a pain to maintain two color schemes and everyone
is probably using dark mode anyway. If you really want it, let me know on GitHub.
\`
},
{
version: 121,
text: \`
Added a slideshow mode. Click \${helpers.createIcon("wallpaper")} at the top.
<p>
Added an option to pan images as they're viewed.
<p>
Double-clicking images now toggles fullscreen.
<p>
The background is now fully black when viewing an image, for better contrast. Other screens are still dark grey.
<p>
Added an option to bookmark privately by default, such as when bookmarking by selecting
a bookmark tag.
<p>
Reworked the animation UI.
\`
},
{
version: 117,
text: \`
Added Linked Tabs. Enable linked tabs in preferences to show images
on more than one monitor as they're being viewed (try it with a portrait monitor).
<p>
Showing the popup menu when Ctrl is pressed is now optional.
\`
},
{
version: 112,
text: \`
Added Send to Tab to the context menu, which allows quickly sending an image to
another tab.
<p>
Added a More Options dropdown to the popup menu. This includes some things that
were previously only available from the hover UI. Send to Tab is also in here.
<p>
Disabled the "Similar Illustrations" lightbulb button on thumbnails. It can now be
accessed from the popup menu, along with a bunch of other ways to get image recommendations.
\`
},
{
version: 110,
text: \`
Added Quick View. This views images immediately when the mouse is pressed,
and images can be panned with the same press.
<p>
This can be enabled in preferences, and may become the default in a future release.
\`
},
{
version: 109,
boring: true,
text: \`Added a visual marker on thumbnails to show the last image you viewed.\`
},
{
version: 104,
text: \`
Bookmarks can now be shuffled, to view them in random order.
<p>
Bookmarking an image now always likes it, like Pixiv's mobile app.
(Having an option for this didn't seem useful.)
<p>
Added a Recent History search, to show recent search results. This can be turned
off in settings.
\`
},
{
version: 102,
boring: true,
text: "Animations now start playing much faster."
},
{
version: 100,
text: \`
Enabled auto-liking images on bookmark by default, to match the Pixiv mobile apps.
If you've previously changed this in preferences, your setting should stay the same.
<p>
Added a download button for the current page when viewing manga posts.
\`
},
{
version: 97,
text: \`
Holding Ctrl now displays the popup menu, to make it easier to use for people on touchpads.<p>
<p>
Keyboard hotkeys reworked, and can now be used while hovering over search results.<p>
<pre>
Ctrl-V - like image
Ctrl-B - bookmark
Ctrl-Alt-B - bookmark privately
Ctrl-Shift-B - remove bookmark
Ctrl-Alt-Shift-M - add bookmark tag
Ctrl-F - follow
Ctrl-Alt-F - follow privately
Ctrl-Shift-F - unfollow</pre>
\`
},
{
version: 89,
text: \`
Reworked zooming to make it more consistent and easier to use.<p>
<p>
You can now zoom images to 100% to view them at actual size.
\`
},
{
version: 82,
text: "Press Ctrl-Alt-Shift-B to bookmark an image with a new tag."
},
{
version: 79,
text: "Added support for viewing new R-18 works by followed users."
},
{
version: 77,
text: \`
Added user searching.
<p>
Commercial/subscription links in user profiles (Fanbox, etc.) now use a different icon.
\`
},
{
version: 74,
text: \`
Viewing your followed users by tag is now supported.
<p>
You can now view other people who bookmarked an image, to see what else they've bookmarked.
This is available from the top-left hover menu.
\`
},
{
version: 72,
text: \`
The followed users page now remembers which page you were on if you reload the page, to make
it easier to browse your follows if you have a lot of them.
<p>
Returning to followed users now flashes who you were viewing like illustrations do,
to make it easier to pick up where you left off.
<p>
Added a browser back button to the context menu, to make navigation easier in fullscreen
when the browser back button isn't available.
\`
},
{
version: 68,
text: \`
You can now go to either the first manga page or the page list from search results.
Click the image to go to the first page, or the page count to go to the page list.
<p>
Our button is now in the bottom-left when we're disabled, since Pixiv now puts a menu
button in the top-left and we were covering it up.
\`
},
{
version: 65,
text: \`
Bookmark viewing now remembers which page you were on if the page is reloaded.
<p>
Zooming is now in smaller increments, to make it easier to zoom to the level you want.
\`
},
{
version: 57,
text: \`
Search for similar artists. Click the recommendations item at the top of the artist page,
or in the top-left when viewing an image.
<p>
You can also now view suggested artists.
\`
},
{
version: 56,
text: \`
Tag translations are now supported. This can be turned off in preferences.
<p>
Added quick tag search editing. After searching for a tag, click the edit button
to quickly add and remove tags.
\`
},
{
version: 55,
text: \`
The "original" view is now available in Rankings.
<p>
Hiding the mouse cursor can now be disabled in preferences.
\`
},
{
version: 49,
text: \`
Add "Hover to show UI" preference, which is useful for low-res monitors.
\`
},
{
version: 47,
text: \`
You can now view the users you're following with "Followed Users". This shows each
user's most recent post.
\`
}
];
var WhatsNew = class extends Widget {
static latestHistoryRevision() {
return updateHistory[0].version;
}
static latestInterestingHistoryRevision() {
for (let history2 of updateHistory) {
if (history2.boring)
continue;
return history2.version;
}
throw Error("Couldn't find anything interesting");
}
static handleLastViewedVersion() {
let refresh = () => {
let lastViewedVersion = ppixiv.settings.get("whats-new-last-viewed-version", 0);
lastViewedVersion = parseInt(lastViewedVersion);
let newUpdates = lastViewedVersion < WhatsNew.latestInterestingHistoryRevision();
helpers.html.setDataSet(document.documentElement.dataset, "whatsNewUpdated", newUpdates);
};
refresh();
ppixiv.settings.addEventListener("whats-new-last-viewed-version", refresh);
}
constructor({ ...options } = {}) {
super({ ...options, dialogClass: "whats-new-dialog", header: "Updates", template: \`
<div class=whats-new-dialog>
<div class=contents>
</div>
</div>
\` });
this.root.addEventListener("click", this.onclick);
ppixiv.settings.set("whats-new-last-viewed-version", WhatsNew.latestHistoryRevision());
this.refresh();
}
onclick = (e) => {
let explanationButton = e.target.closest(".explanation-button");
if (explanationButton) {
e.preventDefault();
e.stopPropagation();
let name = e.target.dataset.explanation;
let target = this.root.querySelector(\`.\${name}\`);
target.hidden = false;
}
};
refresh() {
let itemsBox = this.root.querySelector(".contents");
for (let node of itemsBox.querySelectorAll(".item"))
node.remove();
let githubTopURL = "https:/\x2fgithub.com/ppixiv/ppixiv/";
for (let idx = 0; idx < updateHistory.length; ++idx) {
let update = updateHistory[idx];
let previousUpdate = updateHistory[idx + 1];
let entry = this.createTemplate({ name: "item", html: \`
<div class=item>
<a class=rev href=#></a>
<div class=text></span>
</div>
\` });
let rev = entry.querySelector(".rev");
rev.innerText = "r" + update.version;
let previousVersion = previousUpdate ? "r" + previousUpdate.version : "r1";
rev.href = \`\${githubTopURL}/compare/\${previousVersion}...r\${update.version}\`;
entry.querySelector(".text").innerHTML = update.text;
itemsBox.appendChild(entry);
}
}
};
/\x2f web/vview/misc/saved-search-tags.js
var SavedSearchTags = class {
static data() {
return ppixiv.settings.get("recent-tag-searches") || [];
;
}
static getAllGroups({ data = null } = {}) {
let result = /* @__PURE__ */ new Map();
result.set(null, []);
data ??= this.data();
let inGroup = null;
for (let recentTag of data) {
if (recentTag instanceof Object && recentTag.type == "section") {
inGroup = recentTag.name;
result.set(inGroup, []);
continue;
}
result.get(inGroup).push(recentTag);
}
return result;
}
static setAllGroups(groups) {
let data = [];
for (let [name, tagsInGroup] of groups.entries()) {
if (name != null) {
data.push({
type: "section",
name
});
}
for (let tag of tagsInGroup)
data.push(tag);
}
ppixiv.settings.set("recent-tag-searches", data);
window.dispatchEvent(new Event("recent-tag-searches-changed"));
}
static getAllUsedTags() {
let allTags = /* @__PURE__ */ new Set();
for (let groupTags of this.getAllGroups().values()) {
for (let tags of groupTags)
for (let tag of tags.split(" "))
allTags.add(tag);
}
return allTags;
}
static add(tag, { group = null, addToEnd: addToEnd2 = true } = {}) {
if (this._disableAddingSearchTags || tag == "")
return;
let recentTags = ppixiv.settings.get("recent-tag-searches") || [];
if (tag != null) {
if (this.groupNameForTag(tag) != null)
return;
let idx = recentTags.indexOf(tag);
if (idx != -1)
recentTags.splice(idx, 1);
}
if (group == null)
recentTags.unshift(tag);
else {
let [startIdx, endIdx] = this._findGroupRange(group);
if (startIdx == -1) {
console.log(\`Created tag group: \${group}\`);
recentTags.push({
type: "section",
name: group
});
startIdx = endIdx = recentTags.length;
}
if (tag != null) {
if (addToEnd2)
recentTags.splice(endIdx, 0, tag);
else
recentTags.splice(startIdx + 1, 0, tag);
}
}
ppixiv.settings.set("recent-tag-searches", recentTags);
window.dispatchEvent(new Event("recent-tag-searches-changed"));
}
static modifyTag(oldTags, newTags) {
if (oldTags == newTags)
return;
let data = this.data();
if (this.findIndex({ tag: newTags, data }) != -1) {
ppixiv.message.show(\`Saved tag already exists\`);
return;
}
let idx = this.findIndex({ tag: oldTags, data });
if (idx == -1)
return;
data[idx] = newTags;
ppixiv.settings.set("recent-tag-searches", data);
window.dispatchEvent(new Event("recent-tag-searches-changed"));
ppixiv.message.show(\`Saved tag updated\`);
}
static _findGroupRange(sectionName, { data } = {}) {
let recentTags = data ?? this.data();
let startIdx = -1;
if (sectionName == null)
startIdx = 0;
else {
for (let idx = 0; idx < recentTags.length; ++idx) {
let group = recentTags[idx];
if (!(group instanceof Object) || group.type != "section")
continue;
if (group.name != sectionName)
continue;
startIdx = idx;
break;
}
}
if (startIdx == -1)
return [-1, -1];
for (let idx = startIdx + 1; idx < recentTags.length; ++idx) {
let group = recentTags[idx];
if (!(group instanceof Object) || group.type != "section")
continue;
return [startIdx, idx];
}
return [startIdx, recentTags.length];
}
static deleteGroup(group) {
let [startIdx, endIdx] = this._findGroupRange(group);
if (startIdx == -1)
return;
let count = endIdx - startIdx;
let recentTags = ppixiv.settings.get("recent-tag-searches") || [];
recentTags.splice(startIdx, count);
ppixiv.settings.set("recent-tag-searches", recentTags);
window.dispatchEvent(new Event("recent-tag-searches-changed"));
ppixiv.message.show(\`Group "\${group}" deleted\`);
}
static renameGroup(from, to) {
let fromIdx = this.findIndex({ group: from });
if (fromIdx == -1)
return;
if (this.findIndex({ group: to }) != -1) {
ppixiv.message.show(\`Group "\${to}" already exists\`);
return;
}
let recentTags = ppixiv.settings.get("recent-tag-searches") || [];
recentTags[fromIdx].name = to;
ppixiv.settings.set("recent-tag-searches", recentTags);
let collapsedGroups = this.getCollapsedTagGroups();
if (collapsedGroups.has(from)) {
collapsedGroups.delete(from);
collapsedGroups.add(to);
ppixiv.settings.set("collapsed-tag-groups", [...collapsedGroups]);
}
window.dispatchEvent(new Event("recent-tag-searches-changed"));
}
static moveGroup(group, { down }) {
let data = ppixiv.settings.get("recent-tag-searches") || [];
let groups = this.getAllGroups(data);
let tagGroups = Array.from(groups.keys());
let idx = tagGroups.indexOf(group);
if (idx == -1)
return;
let swapWith = idx + (down ? 1 : -1);
if (swapWith < 0 || swapWith >= tagGroups.length)
return;
if (tagGroups[idx] == null || tagGroups[swapWith] == null)
return;
[tagGroups[idx], tagGroups[swapWith]] = [tagGroups[swapWith], tagGroups[idx]];
let newGroups = /* @__PURE__ */ new Map();
for (let group2 of tagGroups) {
newGroups.set(group2, groups.get(group2));
}
this.setAllGroups(newGroups);
}
static getCollapsedTagGroups() {
return new Set(ppixiv.settings.get("collapsed-tag-groups") || []);
}
static setTagGroupCollapsed(groupName, collapse) {
let collapsedGroups = this.getCollapsedTagGroups();
if (collapse == "toggle")
collapse = !collapsedGroups.has(groupName);
if (collapsedGroups.has(groupName) == collapse)
return;
if (collapse)
collapsedGroups.add(groupName);
else
collapsedGroups.delete(groupName);
ppixiv.settings.set("collapsed-tag-groups", [...collapsedGroups]);
window.dispatchEvent(new Event("recent-tag-searches-changed"));
}
static disableAddingSearchTags(value) {
this._disableAddingSearchTags = value;
}
static groupNameForTag(tag) {
let recentTags = ppixiv.settings.get("recent-tag-searches") || [];
let inGroup = null;
for (let recentTag of recentTags) {
if (recentTag instanceof Object && recentTag.type == "section") {
inGroup = recentTag.name;
continue;
}
if (recentTag == tag)
return inGroup;
}
return null;
}
static remove(tag) {
let recentTags = ppixiv.settings.get("recent-tag-searches") || [];
let idx = recentTags.indexOf(tag);
if (idx == -1)
return;
recentTags.splice(idx, 1);
ppixiv.settings.set("recent-tag-searches", recentTags);
window.dispatchEvent(new Event("recent-tag-searches-changed"));
}
static move(tag, to_idx) {
let recentTags = ppixiv.settings.get("recent-tag-searches") || [];
let idx = recentTags.indexOf(tag);
if (idx == -1)
return;
if (idx == to_idx)
return;
if (to_idx > idx)
to_idx--;
recentTags.splice(idx, 1);
recentTags.splice(to_idx, 0, tag);
ppixiv.settings.set("recent-tag-searches", recentTags);
window.dispatchEvent(new Event("recent-tag-searches-changed"));
}
static findIndex({ tag, group, data = null }) {
data ??= ppixiv.settings.get("recent-tag-searches") || [];
for (let idx = 0; idx < data.length; ++idx) {
let recentTag = data[idx];
if (recentTag instanceof Object && recentTag.type == "section") {
if (group != null && recentTag.name == group)
return idx;
} else {
if (tag != null && recentTag == tag)
return idx;
}
}
return -1;
}
};
/\x2f web/vview/misc/key-storage.js
var KeyStorage = class {
constructor(storeName, { upgradeDb = null, version = 1 } = {}) {
this._dbName = storeName;
this._upgradeDb = upgradeDb;
this._storeName = storeName;
this._version = version;
this._failed = false;
}
async dbOp(func) {
if (this._failed)
return null;
let db;
try {
db = await this._openDatabase();
} catch (e) {
console.log("Couldn't open database:", e);
this._failed = true;
return null;
}
try {
return await func(db);
} finally {
db.close();
}
}
async getDbVersion() {
let dbs = await indexedDB.databases();
for (let db of dbs) {
if (db.name == this._dbName)
return db.version;
}
return 0;
}
_openDatabase() {
return new Promise((resolve, reject) => {
let request = indexedDB.open(this._dbName, this._version);
request.onblocked = (e) => {
console.error("Database blocked:", e);
};
request.onupgradeneeded = (e) => {
let db = e.target.result;
if (this._upgradeDb)
this._upgradeDb(e);
else
db.createObjectStore(this._storeName);
};
request.onsuccess = (e) => {
let db = e.target.result;
resolve(db);
};
request.onerror = (e) => {
reject(request.error);
};
});
}
getStore(db, mode = "readwrite") {
let transaction = db.transaction(this._storeName, mode);
return transaction.objectStore(this._storeName);
}
static awaitRequest(request) {
return new Promise((resolve, reject) => {
let abort = new AbortController();
request.addEventListener("success", (e) => {
abort.abort();
resolve(request.result);
}, { signal: abort.signal });
request.addEventListener("error", (e) => {
abort.abort();
reject(request.result);
}, { signal: abort.signal });
});
}
static asyncStoreGet(store, key) {
return new Promise((resolve, reject) => {
let request = store.get(key);
request.onsuccess = (e) => resolve(e.target.result);
request.onerror = reject;
});
}
async get(key, store) {
return await this.dbOp(async (db) => {
return await KeyStorage.asyncStoreGet(this.getStore(db), key);
});
}
async multiGet(keys) {
return await this.dbOp(async (db) => {
let store = this.getStore(db, "readonly");
let promises = [];
for (let key of keys)
promises.push(KeyStorage.asyncStoreGet(store, key));
return await Promise.all(promises);
}) ?? {};
}
static asyncStoreSet(store, key, value) {
return new Promise((resolve, reject) => {
let request = store.put(value, key);
request.onsuccess = resolve;
request.onerror = reject;
});
}
async set(key, value) {
return await this.dbOp(async (db) => {
return KeyStorage.asyncStoreSet(this.getStore(db), key, value);
});
}
async multiSet(data, { overwrite = true } = {}) {
return await this.dbOp(async (db) => {
let store = this.getStore(db);
async function setKey(key, value) {
if (!overwrite) {
let existingKey = await store.getKey(key);
if (existingKey !== void 0) {
return null;
}
}
let request = store.put(value, key);
await KeyStorage.awaitRequest(request);
}
let promises = [];
for (let [key, value] of Object.entries(data))
promises.push(setKey(key, value));
await Promise.all(promises);
});
}
async multiSetValues(data) {
return await this.dbOp(async (db) => {
let store = this.getStore(db);
let promises = [];
for (let item of data) {
let request = store.put(item);
promises.push(KeyStorage.awaitRequest(request));
}
return Promise.all(promises);
});
}
async delete(key) {
return await this.dbOp(async (db) => {
let store = this.getStore(db);
return KeyStorage.awaitRequest(store.delete(key));
});
}
async multiDelete(keys) {
return await this.dbOp(async (db) => {
let store = this.getStore(db);
let promises = [];
for (let key of keys) {
let request = store.delete(key);
promises.push(KeyStorage.awaitRequest(request));
}
return Promise.all(promises);
});
}
async clear() {
return await this.dbOp(async (db) => {
let store = this.getStore(db);
await store.clear();
});
}
};
/\x2f web/vview/misc/tag-translations.js
var TagTranslations = class {
constructor() {
this._db = new KeyStorage("ppixiv-tag-translations");
this._cache = /* @__PURE__ */ new Map();
}
get enabled() {
return !ppixiv.settings.get("disable-translations");
}
async addTranslationsDict(tags, { overwrite = true } = {}) {
let translations = [];
for (let tag of Object.keys(tags)) {
let tagInfo = tags[tag];
let tagTranslation = {};
for (let lang of Object.keys(tagInfo)) {
if (tagInfo[lang] == "")
continue;
tagTranslation[lang] = tagInfo[lang];
}
if (Object.keys(tagTranslation).length > 0) {
translations.push({
tag,
translation: tagTranslation
});
}
}
this.addTranslations(translations, { overwrite });
}
async addTranslations(tagList, { overwrite = true } = {}) {
let data = {};
for (let tag of tagList) {
if ((tag.translation == null || Object.keys(tag.translation).length == 0) && tag.romaji == null)
continue;
let translation = {};
for (let lang of Object.keys(tag.translation || {})) {
let value = tag.translation[lang];
if (value != "")
translation[lang] = value;
}
let tagInfo = {
tag: tag.tag,
translation
};
if (tag.romaji)
tagInfo.romaji = tag.romaji;
data[tag.tag] = tagInfo;
let exists = this._cache.get(tag.tag);
if (translation.en && (overwrite || !exists))
this._cache.set(tag.tag, translation.en);
}
await this._db.multiSet(data, { overwrite });
}
async getTagInfo(tags) {
if (!this.enabled)
return {};
let result = {};
let translations = await this._db.multiGet(tags);
for (let i = 0; i < tags.length; ++i) {
if (translations[i] == null)
continue;
result[tags[i]] = translations[i];
}
return result;
}
async getTranslations(tags, language = "en") {
if (!this.enabled)
return {};
let info = await this.getTagInfo(tags);
let result = {};
for (let tag of tags) {
if (info[tag] == null || info[tag].translation == null)
continue;
let translation = info[tag].translation[language];
if (translation == null)
continue;
result[tag] = translation;
}
for (let tag of tags) {
if (result[tag])
continue;
result[tag] = this._cache.get(tag);
}
return result;
}
async translateTagList(tags, language) {
let splitTags = helpers.pixiv.splitSearchTags(tags);
let tagList = [];
for (let tag of splitTags) {
let [prefix, unprefixedTag] = helpers.pixiv.splitTagPrefixes(tag);
tagList.push(unprefixedTag);
}
let translatedTags = await this.getTranslations(tagList, language);
let result = [];
for (let oneTag of splitTags) {
let prefixAndTag = helpers.pixiv.splitTagPrefixes(oneTag);
let prefix = prefixAndTag[0];
let tag = prefixAndTag[1];
if (translatedTags[tag])
tag = translatedTags[tag];
result.push(prefix + tag);
}
return result;
}
async getTranslation(tag, language = "en") {
let result = this._cache.get(tag);
if (result != null)
return result;
let translatedTags = await this.getTranslations([tag], "en");
if (translatedTags[tag])
return translatedTags[tag];
else
return tag;
}
};
/\x2f web/vview/screen.js
var Screen = class extends Widget {
get screenType() {
return null;
}
handleKeydown(e) {
}
get displayedMediaId() {
return null;
}
applyVisibility() {
}
get active() {
return !this.root.inert;
}
async activate() {
this.root.inert = false;
}
deactivate() {
this.root.inert = true;
}
};
/\x2f web/vview/util/bezier.js
var Bezier2D = class {
static curve(name) {
if (this._curves == null) {
this._curves = {
"ease": new Bezier2D(0.25, 0.1, 0.25, 1),
"linear": new Bezier2D(0, 0, 1, 1),
"ease-in": new Bezier2D(0.42, 0, 1, 1),
"ease-out": new Bezier2D(0, 0, 0.58, 1),
"ease-in-out": new Bezier2D(0.42, 0, 0.58, 1)
};
}
return this._curves[name];
}
constructor(a, b, c, d) {
this.originalData = [a, b, c, d];
this.X = new Quadratic(0, a, c, 1);
this.Y = new Quadratic(0, b, d, 1);
}
GetXSlope(t) {
return 3 * this.X.A * t * t + 2 * this.X.B * t + this.X.C;
}
evaluate(x) {
let x_start = this.X.D;
let x_end = this.X.A + this.X.B + this.X.C + this.X.D;
let t = scale(x, x_start, x_end, 0, 1);
for (let i = 0; i < 100; ++i) {
let guess = this.X.evaluate(t);
let error = x - guess;
if (Math.abs(error) < 1e-4)
break;
let slope = this.GetXSlope(t);
t += error / slope;
}
return this.Y.evaluate(t);
}
static findCurveForVelocity({
targetVelocity,
distance: distance2,
duration
} = {}) {
let min = 0, max = 0.5;
while (max - min > 0.01) {
let t2 = (max + min) / 2;
let curve2 = new Bezier2D(t2, 0.5 - t2, 0.5, 1);
let sampleSeconds = 1 / 60;
let segmentDistance = distance2 * curve2.evaluate(sampleSeconds / (duration / 1e3));
let actualDistancePerSecond = segmentDistance / sampleSeconds;
if (actualDistancePerSecond > targetVelocity)
min = t2;
else
max = t2;
}
let t = (max + min) / 2;
let curve = new Bezier2D(t, 0.5 - t, 0.45, 1);
let easing = \`cubic-bezier(\${t}, \${0.5 - t}, 0.45, 1)\`;
return { curve, easing, t };
}
};
var Quadratic = class {
constructor(X1, X2, X3, X4) {
this.D = X1;
this.C = 3 * (X2 - X1);
this.B = 3 * (X3 - X2) - this.C;
this.A = X4 - X1 - this.C - this.B;
}
evaluate(t) {
return ((this.A * t + this.B) * t + this.C) * t + this.D;
}
};
/\x2f web/vview/actors/property-animation.js
var PropertyAnimation = class extends Actor {
constructor({
node,
property,
propertyStart = 0,
propertyEnd = 1,
onanimationfinished,
onchange = () => {
},
...options
} = {}) {
super({ ...options });
if (!(node instanceof Array))
node = [node];
this.node = node;
this.onanimationfinished = onanimationfinished;
this.onchange = onchange;
this.state = "stopped";
this.property = property;
this.propertyStart = propertyStart;
this.propertyEnd = propertyEnd;
}
shutdown() {
this.stop();
super.shutdown();
}
get position() {
return this._position;
}
set position(offset) {
if (this.playing)
throw new Error("Animation is running");
this._setPosition(offset);
}
_setPosition(position) {
let oldPosition = this._position;
let oldValue = this._propertyValue;
this._position = position;
let value = this._propertyValue = this.propertyValueForPosition(position);
for (let node of this.node)
node.style.setProperty(this.property, value);
this.onchange({ position, value, oldPosition, oldValue });
}
propertyValueForPosition(position) {
return helpers.math.scale(position, 0, 1, this.propertyStart, this.propertyEnd);
}
get currentPropertyValue() {
return this.propertyValueForPosition(this._position);
}
get playing() {
return this._playToken != null;
}
async play({ endPosition = 1, easing = "ease-in-out", duration = 300 } = {}) {
this.animatingTowards = endPosition;
let token = this._playToken = new Object();
let curve = easing instanceof Bezier2D ? easing : Bezier2D.curve(easing);
if (curve == null)
throw new Error(\`Unknown easing curve: \${easing}\`);
let startPosition = this._position;
let startTime = Date.now();
while (1) {
await helpers.other.vsync();
if (token !== this._playToken)
return;
let offset = (Date.now() - startTime) / duration;
offset = helpers.math.clamp(offset, 0, 1);
let offset_with_easing = curve.evaluate(offset);
let newPosition = helpers.math.scale(offset_with_easing, 0, 1, startPosition, endPosition);
if (Math.abs(newPosition - startPosition) < 1e-5)
newPosition = startPosition;
if (Math.abs(newPosition - endPosition) < 1e-5)
newPosition = endPosition;
this._setPosition(newPosition);
if (offset == 1)
break;
}
this.animatingTowards = null;
this._playToken = null;
this.onanimationfinished(this);
}
stop() {
this._playToken = null;
}
};
/\x2f web/vview/util/fling-velocity.js
var FlingVelocity = class {
constructor({ samplePeriod = 0.1 } = {}) {
this.samplePeriod = samplePeriod;
this.reset();
}
addSample({ x = 0, y = 0 } = {}) {
this.samples.push({
delta: { x, y },
time: Date.now() / 1e3
});
this._purge();
}
_purge() {
let deleteBefore = Date.now() / 1e3 - this.samplePeriod;
while (this.samples.length && this.samples[0].time < deleteBefore)
this.samples.shift();
}
reset() {
this.samples = [];
}
getMovementInDirection(direction) {
let distance2 = this.currentDistance;
let velocity = this._getVelocityFromCurrentDistance(distance2);
switch (direction) {
case "up":
return { distance: -distance2.y, velocity: -velocity.y };
case "down":
return { distance: +distance2.y, velocity: +velocity.y };
case "left":
return { distance: -distance2.x, velocity: -velocity.x };
case "right":
return { distance: +distance2.x, velocity: +velocity.x };
default:
throw new Error("Unknown direction:", direction);
}
}
get currentDistance() {
this._purge();
if (this.samples.length == 0)
return { x: 0, y: 0 };
let total = [0, 0];
for (let sample of this.samples) {
total[0] += sample.delta.x;
total[1] += sample.delta.y;
}
return { x: total[0], y: total[1] };
}
get currentVelocity() {
return this._getVelocityFromCurrentDistance(this.currentDistance);
}
_getVelocityFromCurrentDistance(currentDistance) {
let { x, y } = currentDistance;
if (this.samples.length == 0)
return { x: 0, y: 0 };
let duration = Date.now() / 1e3 - this.samples[0].time;
if (duration < 1e-3) {
return { x: 0, y: 0 };
}
x /= duration;
y /= duration;
return { x, y };
}
};
/\x2f web/vview/actors/touch-listener.js
var TouchListener = class extends Actor {
constructor({
element,
parent,
callback,
multi = false
} = {}) {
super({ parent });
this.element = element;
this.callback = callback;
this.multi = multi;
this.pressedPointerIds = /* @__PURE__ */ new Set();
this.element.addEventListener("pointerdown", this.onpointerevent, this._signal);
}
_updateEventsWhilePressed() {
if (this.pressedPointerIds.size > 0) {
window.addEventListener("pointerup", this.onpointerevent, { capture: true, ...this._signal });
window.addEventListener("pointercancel", this.onpointerevent, { capture: true, ...this._signal });
window.addEventListener("blur", this.onblur, this._signal);
} else {
window.removeEventListener("pointerup", this.onpointerevent, { capture: true });
window.removeEventListener("pointercancel", this.onpointerevent, { capture: true });
window.removeEventListener("blur", this.onblur);
}
}
onblur = (event) => {
for (let pointerId of this.pressedPointerIds) {
console.warn(\`window.blur for \${pointerId} fired without a pointer event being cancelled, simulating it\`);
this.onpointerevent(new PointerEvent("pointercancel", {
pointerId,
button: 0,
buttons: 0
}));
}
};
onpointerevent = (event) => {
let isPressed = event.type == "pointerdown";
if (this.pressedPointerIds.has(event.pointerId) == isPressed)
return;
if (!this.multi && isPressed && this.pressedPointerIds.size > 0)
return;
if (isPressed)
this.pressedPointerIds.add(event.pointerId);
else
this.pressedPointerIds.delete(event.pointerId);
this._updateEventsWhilePressed();
event.pressed = isPressed;
this.callback(event);
delete event.pressed;
};
};
/\x2f web/vview/misc/drag-handler.js
var DragHandler = class extends Actor {
constructor({
name = "unnamed",
element,
signal,
confirmDrag = ({ event }) => true,
oncancelled,
onReleasedWithoutDrag = ({ interactive, cancel }) => true,
ondragstart = ({ event }) => true,
ondrag,
ondragend,
pinch = false,
deferredStart = () => true,
deferDelayMs = null,
...options
} = {}) {
super(options);
this.name = name;
this.element = element;
this.pointers = /* @__PURE__ */ new Map();
this.confirmDrag = confirmDrag;
this.onReleasedWithoutDrag = onReleasedWithoutDrag;
this.oncancelled = oncancelled;
this.ondragstart = ondragstart;
this.ondrag = ondrag;
this.ondragend = ondragend;
this.pinch = pinch;
this.deferredStart = deferredStart;
this.deferDelayMs = deferDelayMs;
this._dragStarted = false;
this._dragDelayTimer = null;
signal ??= new AbortController().signal;
this._touchListener = new TouchListener({
parent: this,
element,
multi: true,
callback: this._pointerevent
});
signal.addEventListener("abort", () => this.cancelDrag());
}
shutdown() {
RunningDrags.remove(this);
super.shutdown();
}
_pointerevent = (e) => {
if (RunningDrags.activeDrag && RunningDrags.activeDrag != this)
return;
if (e.pressed) {
if (this.pointers.size == 0) {
if (!this.confirmDrag({ event: e }))
return;
}
this._startDragging(e);
} else {
if (!this.pointers.has(e.pointerId))
return;
this.pointers.delete(e.pointerId);
if (this.pointers.size == 0)
this._stopDragging({ interactive: true, cancel: e.type == "pointercancel" });
}
};
async _startDragging(event) {
this.pointers.set(event.pointerId, {
x: event.clientX,
y: event.clientY,
ignoreNextPointermove: true
});
if (this.pinch && this._dragDelayTimer != null && this.pointers.size > 1) {
realClearTimeout(this._dragDelayTimer);
this._dragDelayTimer = null;
this._commitStartDragging({ event: null });
}
if (this.pointers.size > 1)
return;
window.addEventListener("pointermove", this._pointermove, this._signal);
this._dragStarted = false;
RunningDrags.add(this, ({ otherDragger }) => {
this.cancelDrag();
if (this.oncancelled)
this.oncancelled({ otherDragger });
});
if (!this.deferredStart())
this._commitStartDragging({ event });
else if (this.deferDelayMs != null) {
this._dragDelayTimer = realSetTimeout(() => {
this._dragDelayTimer = null;
this._commitStartDragging({ event: null });
}, this.deferDelayMs);
}
}
async _commitStartDragging({ event }) {
if (this._dragStarted)
return;
if (!this.ondragstart({ event })) {
this._stopDragging();
return;
}
this._dragStarted = true;
RunningDrags.cancelOtherDrags(this);
}
get isDragging() {
return this._dragStarted;
}
cancelDrag() {
this._stopDragging({ interactive: false });
}
_stopDragging({ interactive = false, cancel = false } = {}) {
this.pointers.clear();
window.removeEventListener("pointermove", this._pointermove);
RunningDrags.remove(this);
if (this._dragDelayTimer != null) {
realClearTimeout(this._dragDelayTimer);
this._dragDelayTimer = null;
}
if (this._dragStarted) {
this._dragStarted = false;
if (this.ondragend)
this.ondragend({ interactive, cancel });
} else
this.onReleasedWithoutDrag({ interactive, cancel });
}
_pointermove = (event) => {
let pointerInfo = this.pointers.get(event.pointerId);
if (pointerInfo == null)
return;
if (this.deferDelayMs != null && this._dragDelayTimer != null) {
this.cancelDrag();
return;
}
this._commitStartDragging({ event });
if (!this._dragStarted)
return;
event.preventDefault();
let info = {
event,
first: pointerInfo.ignoreNextPointermove
};
pointerInfo.ignoreNextPointermove = false;
if (this.pinch) {
let previousCenterPos = this._pointerCenterPos;
let previousRadius = this._pointerDistanceFrom(previousCenterPos);
pointerInfo.x = event.clientX;
pointerInfo.y = event.clientY;
let { x, y } = this._pointerCenterPos;
let radius = this._pointerDistanceFrom({ x, y });
let movementX = x - previousCenterPos.x;
let movementY = y - previousCenterPos.y;
info = {
...info,
x,
y,
movementX,
movementY,
radius,
previousRadius
};
} else {
info = {
...info,
movementX: event.movementX,
movementY: event.movementY,
x: event.clientX,
y: event.clientY,
radius: 0,
previousRadius: 0
};
}
this.ondrag(info);
};
get _pointerCenterPos() {
let centerPos = { x: 0, y: 0 };
for (let { x, y } of this.pointers.values()) {
centerPos.x += x;
centerPos.y += y;
}
centerPos.x /= this.pointers.size;
centerPos.y /= this.pointers.size;
return centerPos;
}
_pointerDistanceFrom(pos) {
let result = 0;
for (let { x, y } of this.pointers.values())
result += helpers.math.distance(pos, { x, y });
result /= this.pointers.size;
return result;
}
};
var RunningDrags = class {
static add(dragger, oncancel) {
if (this._activeDrag != null && this._activeDrag != dragger) {
console.log("Adding:", dragger);
console.log("Active:", this._activeDrag);
throw new Error("Can't add a dragger while one is currently active");
}
this.drags.set(dragger, oncancel);
}
static remove(dragger) {
this.drags.delete(dragger);
if (dragger == this._activeDrag)
this._activeDrag = null;
if (this._activeDrag && this.drags.size == 0)
console.error("_activeDrag wasn't cleared", dragger);
}
static cancelOtherDrags(activeDraggers) {
if (this._activeDrag != null) {
console.log("Dragger was active:", this._activeDrag);
throw new Error("Started a drag while another dragger was already active");
}
if (!this.drags.has(activeDraggers)) {
console.log("activeDraggers:", activeDraggers);
throw new Error("Active dragger isn't in the dragger list");
}
console.assert(this._activeDrag == null);
this._activeDrag = activeDraggers;
for (let [dragger, cancelDrag] of this.drags.entries()) {
if (dragger === activeDraggers)
continue;
cancelDrag({ dragger, otherDragger: activeDraggers });
}
}
static get activeDrag() {
return this._activeDrag;
}
};
__publicField(RunningDrags, "drags", /* @__PURE__ */ new Map());
/\x2f web/vview/actors/pointer-listener.js
var _PointerListener = class {
static installGlobalHandler() {
window.addEventListener("pointermove", (e) => {
_PointerListener.latestMousePagePosition = [e.pageX, e.pageY];
_PointerListener.latestMouseClientPosition = [e.clientX, e.clientY];
this.pointerType = e.pointerType;
}, { passive: true, capture: true });
new _PointerListener({
element: window,
buttonMask: 65535,
capture: true,
callback: (e) => {
if (e.pressed) {
_PointerListener._buttons |= 1 << e.mouseButton;
_PointerListener._buttonPointerIds.set(e.mouseButton, e.pointerId);
} else {
_PointerListener._buttons &= ~(1 << e.mouseButton);
_PointerListener._buttonPointerIds.delete(e.mouseButton);
}
}
});
}
constructor({ element, callback, buttonMask = 1, ...options } = {}) {
this.element = element;
this.buttonMask = buttonMask;
this._pointermoveRegistered = false;
this.buttonsDown = 0;
this.callback = callback;
this._eventOptions = options;
let handlingRightClick = (buttonMask & 2) != 0;
this._blockingContextMenuUntilTimer = false;
if (handlingRightClick)
window.addEventListener("contextmenu", this.oncontextmenu, this._eventOptions);
if (options.signal) {
options.signal.addEventListener("abort", (e) => {
if (this.blockContextmenuTimer != null)
realClearTimeout(this.blockContextmenuTimer);
});
}
this.element.addEventListener("pointerdown", this.onpointerevent, this._eventOptions);
this.element.addEventListener("simulatedpointerdown", this.onpointerevent, this._eventOptions);
}
_registerEventsWhilePressed(enable) {
if (this._pointermoveRegistered)
return;
this._pointermoveRegistered = true;
this.element.addEventListener("pointermove", this.onpointermove, this._eventOptions);
window.addEventListener("pointerup", this.onpointerevent, this._eventOptions);
window.addEventListener("pointercancel", this.onpointerevent, this._eventOptions);
}
_unregisterEventsWhilePressed(enable) {
if (!this._pointermoveRegistered)
return;
this._pointermoveRegistered = false;
this.element.removeEventListener("pointermove", this.onpointermove, this._eventOptions);
window.removeEventListener("pointerup", this.onpointerevent, this._eventOptions);
window.removeEventListener("pointercancel", this.onpointerevent, this._eventOptions);
}
_buttonChanged(buttons, event) {
if (buttons)
this._registerEventsWhilePressed();
else
this._unregisterEventsWhilePressed();
let oldButtonsDown = this.buttonsDown;
this.buttonsDown = buttons;
for (let button = 0; button < 5; ++button) {
let mask = 1 << button;
if (!(mask & this.buttonMask))
continue;
let wasPressed = oldButtonsDown & mask;
let isPressed = this.buttonsDown & mask;
if (wasPressed == isPressed)
continue;
event.mouseButton = button;
event.pressed = isPressed;
this.callback(event);
delete event.mouseButton;
delete event.pressed;
if (button == 1) {
if (isPressed && event.defaultPrevented)
this._blockContextMenuUntilRelease = true;
if (!isPressed && (event.defaultPrevented || this._blockContextMenuUntilRelease)) {
this._blockContextMenuUntilRelease = false;
this._blockContextMenuUntilTimer();
}
}
}
}
onpointerevent = (e) => {
this._buttonChanged(e.buttons, e);
};
onpointermove = (e) => {
if (e.button == -1)
return;
this._buttonChanged(e.buttons, e);
};
oncontextmenu = (e) => {
if (this._blockContextMenuUntilRelease || this._blockingContextMenuUntilTimer) {
e.preventDefault();
e.stopPropagation();
}
};
_blockContextMenuUntilTimer() {
this._blockingContextMenuUntilTimer = true;
if (this.blockContextmenuTimer != null) {
realClearTimeout(this.blockContextmenuTimer);
this.blockContextmenuTimer = null;
}
this.blockContextmenuTimer = realSetTimeout(() => {
this.blockContextmenuTimer = null;
this._blockingContextMenuUntilTimer = false;
}, 50);
}
checkMissedClicks() {
if (!(this.buttonMask & _PointerListener.buttons))
return;
let nodeUnderCursor = document.elementFromPoint(_PointerListener.latestMouseClientPosition[0], _PointerListener.latestMouseClientPosition[1]);
if (nodeUnderCursor == null || !helpers.html.isAbove(this.element, nodeUnderCursor))
return;
for (let button = 0; button < 8; ++button) {
let mask = 1 << button;
if (!(mask & _PointerListener.buttons))
continue;
let newButtonMask = this.buttonsDown;
newButtonMask |= mask;
let e = new MouseEvent("simulatedpointerdown", {
buttons: newButtonMask,
pageX: _PointerListener.latestMousePagePosition[0],
pageY: _PointerListener.latestMousePagePosition[1],
clientX: _PointerListener.latestMousePagePosition[0],
clientY: _PointerListener.latestMousePagePosition[1],
timestamp: performance.now()
});
e.pointerId = _PointerListener._buttonPointerIds.get(button);
this.element.dispatchEvent(e);
}
}
};
var PointerListener = _PointerListener;
__publicField(PointerListener, "latestMousePagePosition", [window.innerWidth / 2, window.innerHeight / 2]);
__publicField(PointerListener, "latestMouseClientPosition", [window.innerWidth / 2, window.innerHeight / 2]);
__publicField(PointerListener, "pointerType", "mouse");
__publicField(PointerListener, "_buttons", 0);
__publicField(PointerListener, "_buttonPointerIds", /* @__PURE__ */ new Map());
/\x2f web/vview/widgets/click-outside-listener.js
var ClickOutsideListener = class extends Actor {
constructor(nodeList, callback) {
super({});
this.nodeList = nodeList;
this.callback = callback;
new PointerListener({
element: document.documentElement,
buttonMask: 65535,
callback: this.windowPointerdown,
...this._signal
});
}
_isNodeInList(node) {
for (const ancestor of this.nodeList) {
if (ancestor instanceof Actor) {
for (const rootNode of ancestor.getRoots())
if (helpers.html.isAbove(rootNode, node))
return true;
} else {
if (helpers.html.isAbove(ancestor, node))
return true;
}
}
return false;
}
windowPointerdown = (e) => {
if (e.button !== 0 && e.button !== 2)
return;
if (this._isNodeInList(e.target))
return;
e.partiallyHandled = true;
this.callback(e.target, { event: e });
};
};
/\x2f web/vview/actors/widget-dragger.js
var WidgetDragger = class extends Actor {
constructor({
name = "widget-dragger",
nodes,
dragNode,
size,
animatedProperty = null,
animatedPropertyInverted = false,
closeIfOutside = null,
confirmDrag = () => true,
onactive = () => {
},
oninactive = () => {
},
ondragstart = () => {
},
ondragend = () => {
},
onanimationstart = () => {
},
onanimationfinished = () => {
},
onbeforeshown = () => {
},
onafterhidden = () => {
},
oncancelled,
onstatechange = () => {
},
visible = false,
direction = "down",
duration = 150,
startOffset = 0,
endOffset = 1,
...options
} = {}) {
super(options);
this._visible = visible;
this.nodes = nodes;
this.onactive = onactive;
this.oninactive = oninactive;
this.ondragstart = ondragstart;
this.ondragend = ondragend;
this.onanimationstart = onanimationstart;
this.onanimationfinished = onanimationfinished;
this.onbeforeshown = onbeforeshown;
this.onafterhidden = onafterhidden;
this.onstatechange = onstatechange;
this.confirmDrag = confirmDrag;
this.animatedProperty = animatedProperty;
this.animatedPropertyInverted = animatedPropertyInverted;
this.closeIfOutside = closeIfOutside;
this.duration = duration;
this.startOffset = startOffset;
this.endOffset = endOffset;
this._state = "idle";
this._runningNonInterruptibleAnimation = false;
if (!(this.duration instanceof Function))
this.duration = () => duration;
if (direction != "up" && direction != "down" && direction != "left" && direction != "right")
throw new Error(\`Invalid drag direction: \${direction}\`);
let vertical = direction == "up" || direction == "down";
let reversed = direction == "left" || direction == "up";
this._recentPointerMovement = new FlingVelocity({ samplePeriod: 0.15 });
this._recentValueMovement = new FlingVelocity({ samplePeriod: 0.15 });
let propertyStart = animatedPropertyInverted ? 1 : 0;
let propertyEnd = animatedPropertyInverted ? 0 : 1;
this._dragAnimation = new PropertyAnimation({
parent: this,
node: this.nodes,
property: this.animatedProperty,
propertyStart,
propertyEnd,
startOffset: this.startOffset,
endOffset: this.endOffset,
onanimationfinished: (anim) => {
if (anim.position < 1e-5)
this._setVisible(false);
this.dragger.cancelDrag();
this._setState("idle");
},
onchange: ({ value, oldValue }) => {
if (oldValue == null)
return;
let delta = Math.abs(value - oldValue);
this._recentValueMovement.addSample({ x: delta });
}
});
this._dragAnimation.position = visible ? 1 : 0;
this.dragger = new DragHandler({
parent: this,
name,
element: dragNode,
oncancelled,
ondragstart: ({ event }) => {
if (!vertical && helpers.shouldIgnoreHorizontalDrag(event))
return false;
let dragIsVertical = Math.abs(event.movementY) > Math.abs(event.movementX);
if (vertical != dragIsVertical)
return false;
let movement = vertical ? event.movementY : event.movementX;
if (reversed)
movement *= -1;
let towardsShown = movement > 0;
if (towardsShown && this.position == 1)
return false;
if (!towardsShown && this.position == 0)
return false;
if (this._runningNonInterruptibleAnimation) {
console.log("Not dragging because a non-interruptible animation is in progress");
return false;
}
if (!this.confirmDrag({ event }))
return false;
this._dragAnimation.stop();
this._recentPointerMovement.reset();
this._setState("dragging");
this._setVisible(true);
this._dragStartedAt = this.position;
return true;
},
ondrag: ({ event, first }) => {
if (this._runningNonInterruptibleAnimation) {
console.log("Not dragging because a non-interruptible animation is in progress");
return false;
}
if (this._state == "animating") {
console.log("animation interrupted by drag");
this._dragAnimation.stop();
this._setState("dragging");
}
if (this._state != "dragging")
this._logStateChanges(\`Expected dragging, in \${this._state}\`);
console.assert(this._state == "dragging", this._state);
this._recentPointerMovement.addSample({ x: event.movementX, y: event.movementY });
if (first)
return;
this._dragAnimation.stop();
let pos = this._dragAnimation.position;
let movement = vertical ? event.movementY : event.movementX;
if (reversed)
movement *= -1;
let actualSize = size;
if (actualSize instanceof Function)
actualSize = actualSize();
pos += movement / actualSize;
pos = helpers.math.clamp(pos, this.startOffset, this.endOffset);
this._dragAnimation.position = pos;
},
ondragend: ({ cancel }) => {
if (cancel) {
if (this._dragStartedAt > 0.5)
this.show();
else
this.hide();
return;
}
let { velocity } = this._recentPointerMovement.getMovementInDirection(direction);
let threshold = 150;
if (velocity > threshold)
return this.show({ velocity });
else if (velocity < -threshold)
return this.hide({ velocity: -velocity });
let open = this._dragAnimation.position > 0.5;
if (open)
this.show({ velocity });
else
this.hide({ velocity: -velocity });
}
});
}
get state() {
return this._state;
}
get visible() {
return this._visible;
}
get position() {
return this._dragAnimation.position;
}
_setVisible(value) {
if (this._visible == value)
return;
this._visible = value;
if (this._visible)
this.onbeforeshown();
else
this.onafterhidden();
if (this.closeIfOutside) {
if (this._visible && this._clickedOutsideListener == null) {
this._clickedOutsideListener = new ClickOutsideListener(this.closeIfOutside, () => this.hide());
} else if (!this._visible && this._clickedOutsideListener != null) {
this._clickedOutsideListener.shutdown();
this._clickedOutsideListener = null;
}
}
}
show({ easing = null, transition = true, interruptible = true } = {}) {
this._animateTo({ endPosition: 1, easing, transition, interruptible });
}
hide({ easing = null, transition = true, interruptible = true } = {}) {
this._animateTo({ endPosition: 0, easing, transition, interruptible });
}
_animateTo({ endPosition, easing = null, transition = true, interruptible = true } = {}) {
if (this._runningNonInterruptibleAnimation) {
console.log("Not running animation because a non-interruptible one is already in progress");
return;
}
if (!transition) {
this._dragAnimation.stop();
this._dragAnimation.position = endPosition;
this._setVisible(endPosition > 0);
this._setState("idle");
return;
}
if (this._state == "idle" && this._dragAnimation.position == endPosition)
return;
this._runningNonInterruptibleAnimation = !interruptible;
if (this._state == "animating" && this._dragAnimation.animatingTowards == endPosition)
return;
if (endPosition > 0)
this._setVisible(true);
let duration = this.duration();
if (easing == null) {
let propertyVelocity = this._recentValueMovement.currentVelocity.x;
let propertyStart = this._dragAnimation.currentPropertyValue;
let propertyEnd = this._dragAnimation.propertyValueForPosition(endPosition);
easing = Bezier2D.findCurveForVelocity({
distance: Math.abs(propertyEnd - propertyStart),
duration,
targetVelocity: Math.abs(propertyVelocity)
}).curve;
}
let promise = this._animationPromise = this._dragAnimation.play({ endPosition, easing, duration });
this._animationPromise.then(() => {
if (promise == this._animationPromise) {
this._animationPromise = null;
this._runningNonInterruptibleAnimation = false;
}
});
this._setState("animating");
}
_recordStateChange(from, to) {
{
this._stateStacks ??= [];
try {
throw new Error();
} catch (e) {
this._stateStacks.push([from, to, e.stack]);
let max = 10;
if (this._stateStacks.length > max)
this._stateStacks.splice(this._stateStacks.length - max);
}
}
}
_logStateChanges(message) {
if (!this._stateStacks)
return;
console.error("Error:", message);
for (let [from, to, stack] of this._stateStacks) {
console.log(\`From \${from} to \${to}, stack:\`);
console.log(stack);
}
}
_setState(state, ...args) {
if (state == this._state)
return;
if (state != "idle" && this._changeState("idle", "active"))
this.onactive(...args);
if (state != "dragging" && this._changeState("dragging", "active"))
this.ondragend(...args);
if (state != "animating" && this._changeState("animating", "active"))
this.onanimationfinished(...args);
if (state == "dragging" && this._changeState("active", "dragging"))
this.ondragstart(...args);
if (state == "animating" && this._changeState("active", "animating"))
this.onanimationstart(...args);
if (state == "idle" && this._changeState("active", "idle"))
this.oninactive(...args);
}
_changeState(oldState, newState) {
if (this._state != oldState)
return false;
this._recordStateChange(this._state, newState);
this._state = newState;
if (newState != "active")
this.onstatechange();
return true;
}
toggle() {
if (this.visible)
this.hide();
else
this.show();
}
get isAnimationPlaying() {
return this._state == "animating";
}
get isAnimatingToShown() {
if (this._state != "animating")
return false;
return this._dragAnimation.animatingTowards == 1;
}
get finished() {
return this._animationPromise;
}
};
/\x2f web/vview/widgets/dialog.js
var _DialogWidget = class extends Widget {
static get topDialog() {
return this.activeDialogs[this.activeDialogs.length - 1];
}
static _updateBlockTouchScrolling() {
if (!window.ppixiv?.ios)
return;
if (_DialogWidget.activeDialogs.length == 0) {
if (this._removeTouchScrollerEvents != null) {
this._removeTouchScrollerEvents.abort();
this._removeTouchScrollerEvents = null;
}
return;
}
if (this._removeTouchScrollerEvents)
return;
this._removeTouchScrollerEvents = new AbortController();
window.addEventListener("touchmove", (e) => {
let topDialog = _DialogWidget.topDialog;
let dialog = topDialog.root.querySelector(".dialog");
if (!helpers.html.isAbove(dialog, e.target))
e.preventDefault();
}, { capture: true, passive: false, signal: this._removeTouchScrollerEvents.signal });
}
constructor({
classes = null,
container = null,
dialogType = "normal",
dialogClass = null,
header = null,
allowClose = true,
showCloseButton = true,
small = null,
backIcon = false,
template,
...options
}) {
if (small == null)
small = dialogType == "small";
if (container == null)
container = document.body;
console.assert(dialogType == "normal" || dialogType == "small");
if (dialogClass == null)
dialogClass = dialogType == "normal" ? "dialog-normal" : "dialog-small";
let closeIcon = backIcon ? "arrow_back_ios_new" : "close";
super({
container,
template: \`
<div class="\${dialogClass}">
<div class="dialog \${classes ?? ""}">
<div class=header>
<div class="close-button-container">
<div class="close-button icon-button">
\${helpers.createIcon(closeIcon)}
</div>
</div>
<span class=header-text></span>
<div class=center-header-helper></div>
</div>
<div class="scroll vertical-scroller">
\${template}
</div>
</div>
</div>
\`,
...options
});
if (window.ppixiv?.mobile)
showCloseButton = false;
if (!this.visible)
throw new Error("Dialog shouldn't be hidden");
this.small = small;
helpers.html.setClass(this.root, "small", this.small);
helpers.html.setClass(this.root, "large", !this.small);
this.dragToExit = true;
this.refreshDialogMode();
window.addEventListener("resize", () => this.refreshDialogMode(), this._signal);
if (window.ppixiv?.mobile) {
this._dialogDragger = new WidgetDragger({
parent: this,
name: "close-dialog",
nodes: this.root,
dragNode: this.root,
visible: false,
duration: 200,
animatedProperty: "--dialog-visible",
direction: "up",
onbeforeshown: () => this.callVisibilityChanged(),
onafterhidden: () => this.callVisibilityChanged(),
confirmDrag: ({ event }) => {
if (!this.dragToExit)
return false;
let scroll = this.querySelector(".scroll");
if (helpers.html.isAbove(scroll, event.target)) {
if (scroll.scrollTop > 0 || scroll.scrollTop < -25)
return false;
}
return true;
},
size: () => {
return this.querySelector(".dialog").getBoundingClientRect().height;
},
onactive: () => this.root.classList.add("dragging-dialog"),
oninactive: () => this.root.classList.remove("dragging-dialog")
});
}
let oldTopDialog = _DialogWidget.topDialog;
if (oldTopDialog)
oldTopDialog.root.inert = true;
_DialogWidget.activeDialogs.push(this);
OpenWidgets.singleton.set(this, true);
if (!header && !showCloseButton)
this.root.querySelector(".header").hidden = true;
this.allowClose = allowClose;
this.root.querySelector(".close-button").hidden = !allowClose || !showCloseButton;
this.header = header;
window.addEventListener("keydown", this._onkeypress.bind(this), { signal: this.shutdownSignal });
if (this.allowClose) {
this.root.addEventListener("click", (e) => {
if (e.target != this.root)
return;
this.visible = false;
});
let closeButton = this.root.querySelector(".close-button");
if (closeButton)
closeButton.addEventListener("click", (e) => {
this.visible = false;
});
window.addEventListener("screenchanged", (e) => {
this.shutdown();
}, { signal: this.shutdownSignal });
if (this._close_on_popstate) {
window.addEventListener("pp:popstate", (e) => {
this.shutdown();
}, { signal: this.shutdownSignal });
}
}
_DialogWidget._updateBlockTouchScrolling();
}
afterInit() {
if (this._dialogDragger)
this._dialogDragger.show();
super.afterInit();
}
get _close_on_popstate() {
return true;
}
set header(value) {
this.root.querySelector(".header-text").textContent = value ?? "";
}
refreshDialogMode() {
helpers.html.setClass(this.root, "floating", !helpers.other.isPhone() || this.small);
}
visibilityChanged() {
super.visibilityChanged();
if (!this.actuallyVisible)
this.shutdown();
}
_onkeypress(e) {
let idx = _DialogWidget.activeDialogs.indexOf(this);
if (idx == -1) {
console.error("Widget isn't in activeDialogs during keypress:", this);
return;
}
if (idx != _DialogWidget.activeDialogs.length - 1)
return;
if (this._handleKeydown(e)) {
e.preventDefault();
e.stopPropagation();
}
}
_handleKeydown(e) {
if (this.allowClose && e.key == "Escape") {
this.visible = false;
return true;
}
return false;
}
get actuallyVisible() {
if (this._dialogDragger)
return this._dialogDragger.visible;
else
return super.visible;
}
async applyVisibility() {
if (this._dialogDragger == null || this._visible) {
super.applyVisibility();
return;
}
this._dialogDragger.hide({ interruptible: false });
}
visibilityChangePromise() {
return this._dialogDragger?.finished;
}
shutdown() {
let idx = _DialogWidget.activeDialogs.indexOf(this);
if (idx == -1)
console.error("Widget isn't in activeDialogs when shutting down:", this);
else
_DialogWidget.activeDialogs.splice(idx, 1);
OpenWidgets.singleton.set(this, false);
_DialogWidget._updateBlockTouchScrolling();
let newTopDialog = _DialogWidget.topDialog;
if (newTopDialog)
newTopDialog.root.inert = false;
super.shutdown();
}
};
var DialogWidget = _DialogWidget;
__publicField(DialogWidget, "activeDialogs", []);
/\x2f web/vview/widgets/prompts.js
var TextPrompt = class extends DialogWidget {
static async prompt(options) {
let prompt = new this(options);
return await prompt.result;
}
constructor({
title,
value = "",
...options
} = {}) {
super({ ...options, dialogClass: "text-entry-popup", small: true, header: title, template: \`
<div class=input-box>
<div class=editor contenteditable></div>
<span class=submit-button>\${helpers.createIcon("mat:check")}</span>
</div>
\` });
this.result = new Promise((completed, cancelled) => {
this._completed = completed;
});
this.input = this.root.querySelector(".editor");
this.input.appendChild(document.createTextNode(value));
this.root.querySelector(".submit-button").addEventListener("click", this.submit);
}
_handleKeydown = (e) => {
if (super._handleKeydown(e))
return true;
if (e.key == "Enter") {
this.submit();
return true;
}
return false;
};
visibilityChanged() {
super.visibilityChanged();
if (this.visible) {
window.addEventListener("keydown", this.onkeydown, { signal: this.visibilityAbort.signal });
this.input.focus();
let size = this.input.firstChild.length;
window.getSelection().setBaseAndExtent(this.input.firstChild, size, this.input.firstChild, size);
} else {
this._completed(null);
}
}
submit = () => {
let result = this.input.textContent;
this._completed(result);
this.visible = false;
};
};
var ConfirmPrompt = class extends DialogWidget {
static async prompt(options) {
let prompt = new this(options);
return await prompt.result;
}
constructor({
header,
text,
...options
} = {}) {
super({
...options,
dialogClass: "confirm-dialog",
allowClose: false,
small: true,
header,
template: \`
<div class=text hidden></div>
<div class=input-box>
\${helpers.createBoxLink({
label: "Yes",
icon: "image",
classes: ["yes"]
})}
\${helpers.createBoxLink({
label: "No",
icon: "image",
classes: ["no"]
})}
</div>
\`
});
if (text) {
let textNode = this.root.querySelector(".text");
textNode.innerText = text;
textNode.hidden = false;
}
this.result = new Promise((completed, cancelled) => {
this._completed = completed;
});
this.root.querySelector(".yes").addEventListener("click", () => this.submit(true), { signal: this.shutdownSignal });
this.root.querySelector(".no").addEventListener("click", () => this.submit(false), { signal: this.shutdownSignal });
}
onkeydown = (e) => {
if (e.key == "Escape") {
e.preventDefault();
e.stopPropagation();
this.submit(false);
}
if (e.key == "Enter") {
e.preventDefault();
e.stopPropagation();
this.submit(true);
}
};
visibilityChanged() {
super.visibilityChanged();
if (this.visible) {
window.addEventListener("keydown", this.onkeydown, { signal: this.visibilityAbort.signal });
} else {
this._completed(null);
}
}
submit = (result) => {
this._completed(result);
this.visible = false;
};
};
/\x2f web/vview/misc/local-api.js
var local_api_exports = {};
__export(local_api_exports, {
LocalBroadcastChannel: () => LocalBroadcastChannel,
default: () => LocalAPI
});
/\x2f web/vview/util/path.js
var Path = class {
static _arrayStartsWith(array, prefix) {
if (array.length < prefix.length)
return false;
for (let i = 0; i < prefix.length; ++i)
if (array[i] != prefix[i])
return false;
return true;
}
static isRelativeTo(path, root) {
let pathParts = path.split("/");
let rootParts = root.split("/");
return Path._arrayStartsWith(pathParts, rootParts);
}
static splitPath(path) {
if (path.endsWith("/"))
path = path.substr(0, path.length - 1);
let parts = path.split("/");
return parts;
}
static getRelativePath(relativeTo, absolutePath) {
console.assert(absolutePath.startsWith("/"));
console.assert(relativeTo.startsWith("/"));
let pathParts = Path.splitPath(absolutePath);
let rootParts = Path.splitPath(relativeTo);
if (!Path._arrayStartsWith(pathParts, rootParts))
return absolutePath;
let relativeParts = pathParts.splice(rootParts.length);
return relativeParts.join("/");
}
static getChild(path, child) {
if (child.startsWith("/"))
return child;
let pathParts = Path.splitPath(path);
let childParts = Path.splitPath(child);
let combined = pathParts.concat(childParts);
return combined.join("/");
}
};
/\x2f web/vview/misc/local-api.js
var LocalAPI = class {
static get localUrl() {
if (!ppixiv.native)
return null;
return new URL("/", document.location);
}
static get path() {
if (ppixiv.native)
return "/";
else
return "/local/";
}
static async localPostRequest(pathname, data = {}, options = {}) {
let url = LocalAPI.localUrl;
if (url == null)
throw Error("Local API isn't enabled");
url.pathname = encodeURI(pathname);
let result = await helpers.pixivRequest.sendPixivRequest({
method: "POST",
url: url.toString(),
responseType: "json",
data: JSON.stringify(data),
signal: options.signal
});
if (result == null)
result = { error: true, reason: "Invalid response" };
return result;
}
static isEnabled() {
return LocalAPI.localUrl != null;
}
static isVVbrowser() {
return navigator.userAgent.indexOf("VVbrowser/") != -1;
}
static async loadMediaInfo(mediaId, { refreshFromDisk = false } = {}) {
let mediaInfo = await LocalAPI.localPostRequest(\`/api/illust/\${mediaId}\`, {
refresh_from_disk: refreshFromDisk
});
return mediaInfo;
}
static async loadRecentBookmarkTags() {
let result = await LocalAPI.localPostRequest(\`/api/bookmark/tags\`);
if (!result.success) {
console.log("Error fetching bookmark tag counts");
return;
}
let tags = [];
for (let tag of Object.keys(result.tags)) {
if (tag == "")
continue;
tags.push(tag);
}
tags.sort();
return tags;
}
static getArgsForId(mediaId, args) {
if (args.path != LocalAPI.path || args.path != "/") {
args.path = LocalAPI.path;
args.query = new URLSearchParams();
args.hash = new URLSearchParams();
args.hashPath = "/";
}
let { type, id: path } = helpers.mediaId.parse(mediaId);
if (type == "file") {
if (!args.hashPath || !Path.isRelativeTo(path, args.hashPath)) {
let parentFolderMediaId = LocalAPI.getParentFolder(mediaId);
args.hashPath = helpers.mediaId.parse(parentFolderMediaId).id;
;
}
let relativePath = Path.getRelativePath(args.hashPath, path);
args.hash.set("file", relativePath);
return args;
}
args.hash.delete("file");
args.query.delete("p");
args.hashPath = path;
return args;
}
static getLocalIdFromArgs(args, { getFolder = false } = {}) {
let root = args.hashPath;
let file = args.hash.get("file");
if (file == null || getFolder)
return "folder:" + root;
if (!file.startsWith("/"))
file = Path.getChild(root, file);
return "file:" + file;
}
static getSearchOptionsForArgs(args) {
let searchOptions = {};
let title = null;
let search_root = helpers.strings.getPathSuffix(args.hashPath, 2);
if (args.hash.has("search")) {
searchOptions.search = args.hash.get("search");
title = "Search: " + searchOptions.search;
}
if (args.hash.has("bookmark-tag")) {
searchOptions.bookmarked = true;
searchOptions.bookmark_tags = args.hash.get("bookmark-tag");
if (searchOptions.bookmark_tags != "")
title = \`Bookmarks tagged \${searchOptions.bookmark_tags}\`;
else
title = \`Untagged bookmarks\`;
} else if (args.hash.has("bookmarks") || LocalAPI.localInfo.bookmark_tag_searches_only) {
searchOptions.bookmarked = true;
title = "Bookmarks";
}
if (args.hash.has("type")) {
searchOptions.media_type = args.hash.get("type");
if (!title)
title = helpers.strings.titleCase(searchOptions.media_type);
}
if (args.hash.has("aspect-ratio")) {
let range = args.hash.get("aspect-ratio");
searchOptions.aspect_ratio = helpers.strings.parseRange(range);
}
if (args.hash.has("pixels")) {
let range = args.hash.get("pixels");
searchOptions.total_pixels = helpers.strings.parseRange(range);
}
if (title == null)
title = "Search";
title += \` inside \${search_root}\`;
if (Object.keys(searchOptions).length == 0) {
searchOptions = null;
let folder_id = LocalAPI.getLocalIdFromArgs(args, { getFolder: true });
let { id } = helpers.mediaId.parse(folder_id);
title = helpers.strings.getPathSuffix(id);
}
return { searchOptions, title };
}
static getParentFolder(mediaId) {
if (mediaId == null || mediaId == "folder:/")
return null;
let { id } = helpers.mediaId.parse(mediaId);
let parts = id.split("/");
if (parts.length == 2)
return "folder:/";
parts.splice(parts.length - 1, 1);
return "folder:" + parts.join("/");
}
static async loadLocalInfo() {
if (LocalAPI.localUrl == null)
return;
this._cachedApiInfo = await LocalAPI.localPostRequest(\`/api/info\`);
}
static get localInfo() {
let info = this._cachedApiInfo;
if (LocalAPI.localUrl == null)
info = { success: false, code: "disabled" };
return {
enabled: LocalAPI.localUrl != null,
local: info.success && info.local,
logged_in: info.success && info.username != "guest",
loginRequired: !info.success && info.code == "access-denied",
bookmark_tag_searches_only: info.tags != null
};
}
static async isLocal() {
let info = await LocalAPI.localPostRequest(\`/api/info\`);
return info.local;
}
static shouldPreloadThumbs(mediaId, url) {
if (ppixiv.mobile)
return true;
if (!helpers.mediaId.isLocal(mediaId))
return true;
if (url != null && LocalAPI._wasThumbnailLoadedRecently(url))
return true;
return false;
}
static async loginRequired() {
let info = await LocalAPI.localPostRequest(\`/api/info\`);
return !info.success && info.code == "access-denied";
}
static async loggedIn() {
let info = await LocalAPI.localPostRequest(\`/api/info\`);
console.log(info);
return info.success && info.username != "guest";
}
static redirectToLogin() {
let query = new URLSearchParams();
query.set("url", document.location.href);
let loginUrl = "/resources/auth.html?" + query.toString();
window.history.replaceState(history.state, "", loginUrl.toString());
document.location.reload();
}
static logout() {
document.cookie = \`auth_token=; max-age=0; path=/\`;
document.location.reload();
}
static addRecentLocalSearch(tag) {
let recentTags = ppixiv.settings.get("local_searches") || [];
let idx = recentTags.indexOf(tag);
if (idx != -1)
recentTags.splice(idx, 1);
recentTags.unshift(tag);
ppixiv.settings.set("local_searches", recentTags);
window.dispatchEvent(new Event("recent-local-searches-changed"));
}
static navigateToTagSearch(tags, { addToHistory = true } = {}) {
tags = tags.trim();
if (tags.length == 0)
tags = null;
if (addToHistory && tags)
LocalAPI.addRecentLocalSearch(tags);
let args = new helpers.args(ppixiv.plocation);
console.assert(args.path == LocalAPI.path);
if (tags)
args.hash.set("search", tags);
else
args.hash.delete("search");
args.set("p", null);
helpers.navigate(args);
}
static async indexFolderForSimilaritySearch(mediaId) {
let { type, id } = helpers.mediaId.parse(mediaId);
if (type != "folder") {
console.log(\`Not a folder: \${mediaId}\`);
return;
}
let result = await LocalAPI.localPostRequest(\`/api/similar/index\`, {
path: id
});
if (!result.success) {
ppixiv.message.show(\`Error indexing \${id}: \${result.reason}\`);
return;
}
ppixiv.message.show(\`Begun indexing \${id} for similarity searching\`);
}
static thumbnailWasLoaded(url) {
this._thumbnailsLoadedRecently ??= /* @__PURE__ */ new Set();
this._thumbnailsLoadedRecently.add(url);
}
static _wasThumbnailLoadedRecently(url) {
return this._thumbnailsLoadedRecently && this._thumbnailsLoadedRecently.has(url);
}
};
var LocalBroadcastChannel = class extends EventTarget {
constructor(name) {
super();
this.name = name;
LocalBroadcastChannelConnection.get.addEventListener(this.name, this.receivedWebSocketsMessage);
this.broadcastChannel = new BroadcastChannel(this.name);
this.broadcastChannel.addEventListener("message", this.receivedBroadcastChannelMessage);
}
receivedWebSocketsMessage = (e) => {
let event = new MessageEvent("message", { data: e.data });
this.dispatchEvent(event);
};
receivedBroadcastChannelMessage = (e) => {
let event = new MessageEvent("message", { data: e.data });
this.dispatchEvent(event);
};
postMessage(data) {
LocalBroadcastChannelConnection.get.send(this.name, data);
this.broadcastChannel.postMessage(data);
}
close() {
LocalBroadcastChannelConnection.get.removeEventListener(this.name, this.receivedWebSocketsMessage);
this.broadcastChannel.removeEventListener("message", this.receivedBroadcastChannelMessage);
}
};
var LocalBroadcastChannelConnection = class extends EventTarget {
static get get() {
if (this.singleton == null)
this.singleton = new LocalBroadcastChannelConnection();
return this.singleton;
}
constructor() {
super();
if (!LocalAPI.isEnabled())
return;
this._sendBuffer = [];
this._reconnectionAttempts = 0;
window.addEventListener("focus", () => {
this._queueReconnect({ reset: true });
});
this._browserId = ppixiv.settings.get("browser_id");
if (this._browserId == null) {
this._browserId = helpers.other.createUuid();
ppixiv.settings.set("browser_id", this._browserId);
console.log("Assigned broadcast browser ID:", this._browserId);
}
this.connect();
}
connect() {
this.disconnect();
let url = new URL("/ws", LocalAPI.localUrl);
url.protocol = document.location.protocol == "https:" ? "wss" : "ws";
this.ws = new WebSocket(url);
this.ws.onopen = this.wsOpened;
this.ws.onclose = this.wsClosed;
this.ws.onerror = this.wsError;
this.ws.onmessage = this.wsMessageReceived;
}
disconnect() {
if (this.ws == null)
return;
this.ws.close();
this.ws = null;
}
_queueReconnect({ reset = false } = {}) {
if (this.ws != null)
return;
if (!reset && this.reconnectId != null)
return;
if (reset) {
if (this.reconnectId != null) {
realClearTimeout(this.reconnectId);
this.reconnectId = null;
}
}
if (reset)
this._reconnectionAttempts = 0;
else
this._reconnectionAttempts++;
this._reconnectionAttempts = Math.min(this._reconnectionAttempts, 5);
let reconnectDelay = Math.pow(this._reconnectionAttempts, 2);
this.reconnectId = realSetTimeout(() => {
this.reconnectId = null;
this.connect();
}, reconnectDelay * 1e3);
}
wsOpened = async (e) => {
console.log("WebSockets connection opened");
if (this.reconnectId != null) {
realClearTimeout(this.reconnectId);
this.reconnectId = null;
}
this._reconnectionAttempts = 0;
this._sendRaw({
"command": "init",
"browser_id": this._browserId
});
this._sendBufferedData();
};
wsClosed = async (e) => {
console.log("WebSockets connection closed", e, e.wasClean, e.reason);
this.disconnect();
this._queueReconnect();
};
wsError = (e) => {
console.log("WebSockets connection error");
};
wsMessageReceived = (e) => {
let message = JSON.parse(e.data);
if (message.command != "receive-broadcast") {
console.error(\`Unknown WebSockets command: \${message.command}\`);
return;
}
let event = new MessageEvent(message.message.channel, { data: message.message.data });
this.dispatchEvent(event);
};
send(channel, message) {
if (!LocalAPI.isEnabled())
return;
let data = {
"command": "send-broadcast",
"browser_id": this._browserId,
"message": {
"channel": channel,
"data": message
}
};
this._sendBuffer.push(data);
this._sendBufferedData();
}
_sendRaw(data) {
this.ws.send(JSON.stringify(data, null, 4));
}
_sendBufferedData() {
if (this.ws == null)
return;
while (this._sendBuffer.length > 0) {
if (this.ws.bufferedAmount > 1024 * 1024 || this.ws.readyState != 1)
break;
let data = this._sendBuffer.shift();
this._sendRaw(data);
}
}
};
/\x2f web/vview/misc/recent-bookmark-tags.js
var RecentBookmarkTags = class {
static setRecentBookmarkTags(tags) {
ppixiv.settings.set("recent-bookmark-tags", JSON.stringify(tags));
}
static getRecentBookmarkTags() {
let recentBookmarkTags = ppixiv.settings.get("recent-bookmark-tags");
if (recentBookmarkTags == null)
return [];
return JSON.parse(recentBookmarkTags);
}
static updateRecentBookmarkTags(tagList) {
let recentBookmarkTags = this.getRecentBookmarkTags();
for (let i = 0; i < tagList.length; ++i) {
let idx = recentBookmarkTags.indexOf(tagList[i]);
if (idx != -1)
recentBookmarkTags.splice(idx, 1);
}
for (let i = 0; i < tagList.length; ++i)
recentBookmarkTags.unshift(tagList[i]);
recentBookmarkTags.splice(100);
this.setRecentBookmarkTags(recentBookmarkTags);
}
};
/\x2f web/vview/util/struct.js
var rechk = /^([<>])?(([1-9]\\d*)?([xcbB?hHiIfdsp]))*\$/;
var refmt = /([1-9]\\d*)?([xcbB?hHiIfdsp])/g;
var str = (v, o, c) => String.fromCharCode(
...new Uint8Array(v.buffer, v.byteOffset + o, c)
);
var rts = (v, o, c, s) => new Uint8Array(v.buffer, v.byteOffset + o, c).set(s.split("").map((str2) => str2.charCodeAt(0)));
var pst = (v, o, c) => str(v, o + 1, Math.min(v.getUint8(o), c - 1));
var tsp = (v, o, c, s) => {
v.setUint8(o, s.length);
rts(v, o + 1, c - 1, s);
};
var lut = (le) => ({
x: (c) => [1, c, 0],
c: (c) => [c, 1, (o) => ({ u: (v) => str(v, o, 1), p: (v, c2) => rts(v, o, 1, c2) })],
"?": (c) => [c, 1, (o) => ({ u: (v) => Boolean(v.getUint8(o)), p: (v, B) => v.setUint8(o, B) })],
b: (c) => [c, 1, (o) => ({ u: (v) => v.getInt8(o), p: (v, b) => v.setInt8(o, b) })],
B: (c) => [c, 1, (o) => ({ u: (v) => v.getUint8(o), p: (v, B) => v.setUint8(o, B) })],
h: (c) => [c, 2, (o) => ({ u: (v) => v.getInt16(o, le), p: (v, h) => v.setInt16(o, h, le) })],
H: (c) => [c, 2, (o) => ({ u: (v) => v.getUint16(o, le), p: (v, H) => v.setUint16(o, H, le) })],
i: (c) => [c, 4, (o) => ({ u: (v) => v.getInt32(o, le), p: (v, i) => v.setInt32(o, i, le) })],
I: (c) => [c, 4, (o) => ({ u: (v) => v.getUint32(o, le), p: (v, I) => v.setUint32(o, I, le) })],
f: (c) => [c, 4, (o) => ({ u: (v) => v.getFloat32(o, le), p: (v, f) => v.setFloat32(o, f, le) })],
d: (c) => [c, 8, (o) => ({ u: (v) => v.getFloat64(o, le), p: (v, d) => v.setFloat64(o, d, le) })],
s: (c) => [1, c, (o) => ({ u: (v) => str(v, o, c), p: (v, s) => rts(v, o, c, s.slice(0, c)) })],
p: (c) => [1, c, (o) => ({ u: (v) => pst(v, o, c), p: (v, s) => tsp(v, o, c, s.slice(0, c - 1)) })]
});
var errbuf = new RangeError("Structure larger than remaining buffer");
var errval = new RangeError("Not enough values for structure");
function struct(format) {
let fns = [], size = 0, m = rechk.exec(format);
if (!m) {
throw new RangeError("Invalid format string");
}
const t = lut("<" === m[1]), lu = (n, c) => t[c](n ? parseInt(n, 10) : 1);
while (m = refmt.exec(format)) {
((r, s, f) => {
for (let i = 0; i < r; ++i, size += s) {
if (f) {
fns.push(f(size));
}
}
})(...lu(...m.slice(1)));
}
const unpackFrom = (arrb, offs) => {
if (arrb.byteLength < (offs | 0) + size) {
throw errbuf;
}
let v = new DataView(arrb, offs | 0);
return fns.map((f) => f.u(v));
};
const packInfo = (arrb, offs, ...values) => {
if (values.length < fns.length) {
throw errval;
}
if (arrb.byteLength < offs + size) {
throw errbuf;
}
const v = new DataView(arrb, offs);
new Uint8Array(arrb, offs, size).fill(0);
fns.forEach((f, i) => f.p(v, values[i]));
};
const pack = (...values) => {
let b = new ArrayBuffer(size);
packInfo(b, 0, ...values);
return b;
};
const unpack = (arrb) => unpackFrom(arrb, 0);
function* iterUnpack(arrb) {
for (let offs = 0; offs + size <= arrb.byteLength; offs += size) {
yield unpackFrom(arrb, offs);
}
}
return Object.freeze({
unpack,
pack,
unpackFrom,
packInfo,
iterUnpack,
format,
size
});
}
/\x2f web/vview/misc/encode-mkv.js
function encodeLength(value) {
return struct(">BI").pack(8, value);
}
function headerInt(container, identifier, value) {
container.push(new Uint8Array(identifier));
let data = struct(">II").pack(0, value);
let size = data.byteLength;
container.push(encodeLength(size));
container.push(data);
}
function headerFloat(container, identifier, value) {
container.push(new Uint8Array(identifier));
let data = struct(">f").pack(value);
let size = data.byteLength;
container.push(encodeLength(size));
container.push(data);
}
function headerData(container, identifier, data) {
container.push(new Uint8Array(identifier));
container.push(encodeLength(data.byteLength));
container.push(data);
}
function totalSize(array) {
let size = 0;
for (let idx = 0; idx < array.length; ++idx) {
let item = array[idx];
size += item.byteLength;
}
return size;
}
function appendArray(a1, a2) {
let result = new Uint8Array(a1.byteLength + a2.byteLength);
result.set(new Uint8Array(a1));
result.set(new Uint8Array(a2), a1.byteLength);
return result;
}
function createDataBlock(identifier, parts) {
identifier = new Uint8Array(identifier);
let dataSize = totalSize(parts);
let encodedDataSize = encodeLength(dataSize);
let result = new Uint8Array(identifier.byteLength + encodedDataSize.byteLength + dataSize);
let pos = 0;
result.set(new Uint8Array(identifier), pos);
pos += identifier.byteLength;
result.set(new Uint8Array(encodedDataSize), pos);
pos += encodedDataSize.byteLength;
for (let i = 0; i < parts.length; ++i) {
let part = parts[i];
result.set(new Uint8Array(part), pos);
pos += part.byteLength;
}
return result;
}
function ebmlHeader() {
let parts = [];
headerInt(parts, [66, 134], 1);
headerInt(parts, [66, 247], 1);
headerInt(parts, [66, 242], 4);
headerInt(parts, [66, 243], 8);
headerData(parts, [66, 130], new Uint8Array([109, 97, 116, 114, 111, 115, 107, 97]));
headerInt(parts, [66, 135], 4);
headerInt(parts, [66, 133], 2);
return createDataBlock([26, 69, 223, 163], parts);
}
function ebmlInfo(duration) {
let parts = [];
headerInt(parts, [42, 215, 177], 1e6);
headerData(parts, [77, 128], new Uint8Array([120]));
headerData(parts, [87, 65], new Uint8Array([120]));
headerFloat(parts, [68, 137], duration * 1e3);
return createDataBlock([21, 73, 169, 102], parts);
}
function ebmlTrackEntryVideo(width, height) {
let parts = [];
headerInt(parts, [176], width);
headerInt(parts, [186], height);
return createDataBlock([224], parts);
}
function ebmlTrackEntry(width, height) {
let parts = [];
headerInt(parts, [215], 1);
headerInt(parts, [115, 197], 1);
headerInt(parts, [131], 1);
headerInt(parts, [156], 0);
headerInt(parts, [35, 227, 131], 33333333);
headerData(parts, [134], new Uint8Array([86, 95, 77, 74, 80, 69, 71]));
parts.push(ebmlTrackEntryVideo(width, height));
return createDataBlock([174], parts);
}
function ebmlTracks(width, height) {
let parts = [];
parts.push(ebmlTrackEntry(width, height));
return createDataBlock([22, 84, 174, 107], parts);
}
function ebmlSimpleblock(frameData) {
let result = new Uint8Array([
129,
0,
0,
128
]);
result = appendArray(result, frameData);
return result;
}
function ebmlCluster(frameData, frameTime) {
let parts = [];
headerInt(parts, [231], Math.round(frameTime * 1e3));
headerData(parts, [163], ebmlSimpleblock(frameData));
return createDataBlock([31, 67, 182, 117], parts);
}
function ebmlCueTrackPositions(filePosition) {
let parts = [];
headerInt(parts, [247], 1);
headerInt(parts, [241], filePosition);
return createDataBlock([183], parts);
}
function ebmlCuePoint(frameTime, filePosition) {
let parts = [];
headerInt(parts, [179], Math.round(frameTime * 1e3));
parts.push(ebmlCueTrackPositions(filePosition));
return createDataBlock([187], parts);
}
function ebmlCues(frameTimes, frameFilePositions) {
let parts = [];
for (let frame = 0; frame < frameFilePositions.length; ++frame) {
let frameTime = frameTimes[frame];
let filePosition = frameFilePositions[frame];
parts.push(ebmlCuePoint(frameTime, filePosition));
}
return createDataBlock([28, 83, 187, 107], parts);
}
function ebmlSegment(parts) {
return createDataBlock([24, 83, 128, 103], parts);
}
var EncodeMKV = class {
constructor(width, height) {
this.width = width;
this.height = height;
this.frames = [];
}
add(data, duration) {
this.frames.push({ data, duration });
}
build() {
let totalDuration = 0;
for (let frame = 0; frame < this.frames.length; ++frame) {
let { duration } = this.frames;
totalDuration += duration / 1e3;
}
let headerParts = ebmlHeader();
let parts = [];
parts.push(ebmlInfo(totalDuration));
parts.push(ebmlTracks(this.width, this.height));
let currentPos = 0;
for (let part of parts)
currentPos += part.byteLength;
let frameFilePositions = [];
let frameFileTimes = [];
let frameTime = 0;
for (let frame = 0; frame < this.frames.length; ++frame) {
let data = this.frames[frame].data;
let ms = this.frames[frame].duration;
let cluster = ebmlCluster(data, frameTime);
parts.push(cluster);
frameFilePositions.push(currentPos);
frameFileTimes.push(frameTime);
frameTime += ms / 1e3;
currentPos += cluster.byteLength;
}
;
parts.push(ebmlCues(frameFileTimes, frameFilePositions));
let segment = ebmlSegment(parts);
let file = [];
file = file.concat(headerParts);
file = file.concat(segment);
return new Blob(file);
}
};
/\x2f web/vview/misc/zip-image-downloader.js
var IncrementalReader = class {
constructor(reader, options = {}) {
this.reader = reader;
this.position = 0;
if ("byteLength" in reader) {
this.inputBuffer = new Int8Array(reader);
this.inputBufferFinished = true;
} else {
this.inputBuffer = new Int8Array(0);
this.inputBufferFinished = false;
}
this.onprogress = options.onprogress;
}
async read(bytes) {
let buffer = new ArrayBuffer(bytes);
let result = new Int8Array(buffer);
let outputPos = 0;
while (outputPos < bytes) {
if (this.inputBuffer.byteLength > 0) {
let copyBytes = Math.min(bytes - outputPos, this.inputBuffer.byteLength);
let buf = new Int8Array(this.inputBuffer.buffer, this.inputBuffer.byteOffset, copyBytes);
result.set(buf, outputPos);
outputPos += copyBytes;
this.inputBuffer = new Int8Array(this.inputBuffer.buffer, this.inputBuffer.byteOffset + copyBytes);
continue;
}
if (this.inputBufferFinished)
throw new Error("Incomplete file");
let { value, done } = await this.reader.read();
if (value == null)
value = new Int8Array(0);
this.inputBufferFinished = done;
this.inputBuffer = value;
if (value)
this.position += value.length;
if (this.onprogress)
this.onprogress(this.position);
}
;
return buffer;
}
};
var ZipImageDownloader = class {
constructor(url, options = {}) {
this.url = url;
this.signal = options.signal;
this.onprogress = options.onprogress;
this.startPromise = this.start();
}
async start() {
let response = await helpers.pixivRequest.sendPixivRequest({
method: "GET",
url: this.url,
responseType: "arraybuffer",
signal: this.signal
});
if (response == null)
return null;
this.totalLength = response.headers.get("Content-Length");
if (this.totalLength != null)
this.totalLength = parseInt(this.totalLength);
let fetchReader;
if (response.body.getReader)
fetchReader = response.body.getReader();
else
fetchReader = await response.arrayBuffer();
this.reader = new IncrementalReader(fetchReader, {
onprogress: (position) => {
if (this.onprogress && this.totalLength > 0) {
let progress = position / this.totalLength;
this.onprogress(progress);
}
}
});
}
async getNextFrame() {
await this.startPromise;
if (this.reader == null)
return null;
let header = await this.reader.read(30);
let view = new DataView(header);
let magic = view.getUint32(0, true);
if (magic == 33639248) {
return null;
}
if (magic != 67324752)
throw Error("Unrecognized file");
let compression = view.getUint16(8, true);
if (compression != 0)
throw Error("Unsupported compression method");
let fileSize = view.getUint32(22, true);
let filenameSize = view.getUint16(26, true);
let extraSize = view.getUint16(28, true);
await this.reader.read(filenameSize);
await this.reader.read(extraSize);
let result = await this.reader.read(fileSize);
let flags = view.getUint16(6, true);
if (flags & 8) {
let descriptor = await this.reader.read(16);
let descriptorView = new DataView(descriptor);
if (descriptorView.getUint32(0, true) != 134695760)
throw Error("Unrecognized file");
}
return result;
}
};
/\x2f web/vview/misc/pixiv-ugoira-downloader.js
var PixivUgoiraDownloader = class {
constructor(illustData, progress) {
this.illustData = illustData;
this.onprogress = progress;
this.metadata = illustData.ugoiraMetadata;
this.mimeType = illustData.ugoiraMetadata.mimeType;
this.frames = [];
this.loadAllFrames();
}
async loadAllFrames() {
ppixiv.message.show(\`Downloading video...\`);
let downloader = new ZipImageDownloader(this.metadata.originalSrc, {
onprogress: (progress) => {
if (!this.onprogress)
return;
try {
this.onprogress.set(progress);
} catch (e) {
console.error(e);
}
}
});
while (1) {
let file = await downloader.getNextFrame();
if (file == null)
break;
this.frames.push(file);
}
ppixiv.message.hide();
let img = document.createElement("img");
let blob = new Blob([this.frames[0]], { type: this.mimeType || "image/png" });
let firstFrameURL = URL.createObjectURL(blob);
img.src = firstFrameURL;
await helpers.other.waitForImageLoad(img);
URL.revokeObjectURL(firstFrameURL);
let width = img.naturalWidth;
let height = img.naturalHeight;
try {
let encoder = new EncodeMKV(width, height);
let frameCount = this.illustData.ugoiraMetadata.frames.length;
for (let frame = 0; frame < frameCount; ++frame) {
let frameData2 = this.frames[frame];
let duration = this.metadata.frames[frame].delay;
encoder.add(frameData2, duration);
}
let frameData = this.frames[frameCount - 1];
encoder.add(frameData, 0);
let mkv = encoder.build();
let filename = this.illustData.userName + " - " + this.illustData.illustId + " - " + this.illustData.illustTitle + ".mkv";
helpers.saveBlob(mkv, filename);
} catch (e) {
console.error(e);
}
if (this.onprogress)
this.onprogress.set(null);
}
};
/\x2f web/vview/util/crc32.js
var crcTable = [];
for (let n = 0; n < 256; n++) {
let c = n;
for (let k = 0; k < 8; k++)
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
crcTable[n] = c;
}
function crc32(buf) {
let crc = 0 ^ -1;
for (let i = 0; i < buf.length; i++)
crc = crc >>> 8 ^ crcTable[(crc ^ buf[i]) & 255];
return crc ^ -1;
}
/\x2f web/vview/misc/create-zip.js
var CreateZIP = class {
constructor(filenames, files) {
if (filenames.length != files.length)
throw "Mismatched array lengths";
let filenameBlobs = [];
for (let i = 0; i < filenames.length; ++i) {
let filename = new Blob([filenames[i]]);
filenameBlobs.push(filename);
}
let blobs = [];
let crc32s = [];
for (let i = 0; i < filenames.length; ++i) {
let data = files[i];
let crc = crc32(new Int8Array(data));
crc32s.push(crc);
blobs.push(new Blob([data]));
}
let parts = [];
let filePos = 0;
let fileOffsets = [];
for (let i = 0; i < filenames.length; ++i) {
let filename = filenameBlobs[i];
let data = blobs[i];
let crc = crc32s[i];
fileOffsets.push(filePos);
let localFileHeader = this.createLocalFileHeader(filename, data, crc);
parts.push(localFileHeader);
filePos += localFileHeader.size;
parts.push(data);
filePos += data.size;
}
let centralDirectoryPos = filePos;
let centralDirectorySize = 0;
for (let i = 0; i < filenames.length; ++i) {
let filename = filenameBlobs[i];
let data = blobs[i];
let crc = crc32s[i];
let fileOffset = fileOffsets[i];
let centralRecord = this.createCentralDirectoryEntry(filename, data, fileOffset, crc);
centralDirectorySize += centralRecord.size;
parts.push(centralRecord);
}
let endCentralRecord = this.createEndCentral(filenames.length, centralDirectoryPos, centralDirectorySize);
parts.push(endCentralRecord);
return new Blob(parts, {
"type": "application/zip"
});
}
createLocalFileHeader(filename, file, crc) {
let data = struct("<IHHHHHIIIHH").pack(
67324752,
10,
0,
0,
0,
0,
crc,
file.size,
file.size,
filename.size,
0
);
return new Blob([data, filename]);
}
createCentralDirectoryEntry(filename, file, fileOffset, crc) {
let data = struct("<IHHHHHHIIIHHHHHII").pack(
33639248,
10,
10,
0,
0,
0,
0,
crc,
file.size,
file.size,
filename.size,
0,
0,
0,
0,
0,
fileOffset
);
return new Blob([data, filename]);
}
createEndCentral(numFiles, centralDirectoryPos, centralDirectorySize) {
let data = struct("<IHHHHIIH").pack(
101010256,
0,
0,
numFiles,
numFiles,
centralDirectorySize,
centralDirectoryPos,
0
);
return new Blob([data]);
}
};
/\x2f web/vview/util/recaptcha.js
var loadPromise = null;
function load() {
if (loadPromise == null)
loadPromise = loadInner();
return loadPromise;
}
async function loadInner() {
if (!ppixiv.pixivInfo?.pixivTests?.recaptcha_follow_user)
return;
if (!ppixiv.pixivInfo?.recaptchaKey) {
console.warn("Pixiv requires recaptcha for this user, but we didn't get a recaptcha key");
return;
}
console.log("Loading recaptcha");
let script = document.realCreateElement("script");
script.src = \`https:/\x2fwww.recaptcha.net/recaptcha/enterprise.js?render=\${ppixiv.pixivInfo.recaptchaKey}\`;
document.head.appendChild(script);
await helpers.other.waitForEvent(script, "load");
script.remove();
console.log("Waiting for recaptcha");
await waitForRecaptchaReady();
console.log("Recaptcha is ready");
window.grecaptcha.enterprise.execute(ppixiv.pixivInfo.recaptchaKey, { action: "www/pageload" });
}
function waitForRecaptchaReady() {
return new Promise((resolve) => {
window.grecaptcha.enterprise.ready(() => resolve());
});
}
async function getRecaptchaToken(action) {
await load();
return await window.grecaptcha.enterprise.execute(ppixiv.pixivInfo.recaptchaKey, { action });
}
/\x2f web/vview/util/gm-download.js
var _downloadPort = null;
function _getDownloadServer() {
if (_downloadPort != null)
return _downloadPort;
_downloadPort = new Promise((accept, reject) => {
let e = new Event("request-download-channel", { cancelable: true });
if (window.dispatchEvent(e)) {
reject("GM.xmlHttpRequest isn't available");
return;
}
let receiveMessagePort = (e2) => {
if (e2.data.cmd != "download-setup")
return;
window.removeEventListener("message", receiveMessagePort);
_downloadPort = e2.ports[0];
accept(e2.ports[0]);
};
window.addEventListener("message", receiveMessagePort);
});
return _downloadPort;
}
function _downloadUsingServer(serverPort, { url, ...args }) {
return new Promise((accept, reject) => {
if (url == null) {
reject(null);
return;
}
url = new URL(url);
let { port1: serverResponsePort, port2: clientResponsePort } = new MessageChannel();
clientResponsePort.onmessage = (e) => {
clientResponsePort.close();
if (e.data.success)
accept(e.data.response);
else
reject(new Error(e.data.error));
};
serverPort.realPostMessage({
url: url.toString(),
...args
}, [serverResponsePort]);
});
}
async function downloadPixivImage(url) {
let server = await _getDownloadServer();
if (server == null)
throw new Error("Downloading not available");
return await _downloadUsingServer(server, {
url,
headers: {
"Cache-Control": "max-age=360000",
Referer: "https:/\x2fwww.pixiv.net/",
Origin: "https:/\x2fwww.pixiv.net/"
}
});
}
async function sendRequest2(args) {
let server = await _getDownloadServer();
if (server == null)
throw new Error("Downloading not available");
return await _downloadUsingServer(server, args);
}
/\x2f web/vview/misc/actions.js
var Actions = class {
static async _bookmarkAddInternal(mediaId, options) {
let illustId = helpers.mediaId.toIllustIdAndPage(mediaId)[0];
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, {
full: false
});
if (options == null)
options = {};
if (!options.disableAutoLike) {
console.log("Automatically liking image with bookmark");
Actions.likeImage(mediaId, true);
}
let wasBookmarked = mediaInfo.bookmarkData != null;
let request = {
illust_id: illustId,
tags: options.tags || [],
restrict: options.private ? 1 : 0
};
let result = await helpers.pixivRequest.post(
"/ajax/illusts/bookmarks/add",
request
);
let newBookmarkId = result.body.last_bookmark_id;
if (newBookmarkId == null)
newBookmarkId = mediaInfo.bookmarkData ? mediaInfo.bookmarkData.id : null;
if (newBookmarkId == null)
throw "Didn't get a bookmark ID";
mediaInfo.bookmarkData = {
id: newBookmarkId,
private: !!request.restrict
};
let e = new Event("bookmarked");
e.illustId = illustId;
window.dispatchEvent(e);
ppixiv.extraCache.updateCachedBookmarkTags(mediaId, request.tags);
console.log(
\`Updated bookmark data for \${mediaId}: id: \${newBookmarkId}, tags: \${request.restrict ? "private" : "public"} \${request.tags.join(" ")}\`
);
if (!wasBookmarked) {
let fullMediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId);
if (fullMediaInfo)
fullMediaInfo.bookmarkCount++;
}
ppixiv.message.show(
wasBookmarked ? "Bookmark edited" : options.private ? "Bookmarked privately" : "Bookmarked"
);
}
static async bookmarkAdd(mediaId, options) {
if (helpers.mediaId.isLocal(mediaId))
return await this._localBookmarkAdd(mediaId, options);
if (options == null)
options = {};
if (options.private == null && ppixiv.settings.get("bookmark_privately_by_default"))
options.private = true;
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, {
full: false
});
console.log(\`Add bookmark for \${mediaId}, options:\`, options);
if (mediaInfo.bookmarkData == null) {
console.log("Initial bookmark");
if (options.tags != null)
RecentBookmarkTags.updateRecentBookmarkTags(options.tags);
return await Actions._bookmarkAddInternal(mediaId, options);
}
if (options.tags == null && options.private == null) {
console.log("Already bookmarked");
return;
}
if (options.tags == null && options.private != null) {
console.log("Only editing private field", options.private);
return await Actions.bookmarkSetPrivate(mediaId, options.private);
}
let bookmarkTags = await ppixiv.extraCache.loadBookmarkDetails(mediaId);
let bookmarkParams = {
disableAutoLike: true
};
if ("private" in options)
bookmarkParams.private = options.private;
else
bookmarkParams.private = mediaInfo.bookmarkData.private;
if ("tags" in options)
bookmarkParams.tags = options.tags;
else
bookmarkParams.tags = bookmarkTags;
if (options.tags != null) {
for (let tag of options.tags) {
let isNewTag = bookmarkTags.indexOf(tag) == -1;
if (isNewTag)
RecentBookmarkTags.updateRecentBookmarkTags([tag]);
}
}
return await Actions._bookmarkAddInternal(mediaId, bookmarkParams);
}
static async bookmarkRemove(mediaId) {
if (helpers.mediaId.isLocal(mediaId))
return await this._localBookmarkRemove(mediaId);
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, {
full: false
});
if (mediaInfo.bookmarkData == null) {
console.log("Not bookmarked");
return;
}
let bookmarkId = mediaInfo.bookmarkData.id;
console.log("Remove bookmark", bookmarkId);
let result = await helpers.pixivRequest.post(
"/ajax/illusts/bookmarks/remove",
{
bookmarkIds: [bookmarkId]
}
);
console.log("Removing bookmark finished");
mediaInfo.bookmarkData = null;
let fullMediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId);
if (fullMediaInfo)
fullMediaInfo.bookmarkCount--;
ppixiv.extraCache.updateCachedBookmarkTags(mediaId, null);
ppixiv.message.show("Bookmark removed");
}
static async _localBookmarkAdd(mediaId, options) {
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, {
full: false
});
let bookmarkOptions = {};
if (options.tags != null)
bookmarkOptions.tags = options.tags;
let wasBookmarked = mediaInfo.bookmarkData != null;
let result = await LocalAPI.localPostRequest(
\`/api/bookmark/add/\${mediaId}\`,
{
...bookmarkOptions
}
);
if (!result.success) {
ppixiv.message.show(\`Couldn't edit bookmark: \${result.reason}\`);
return;
}
ppixiv.extraCache.updateCachedBookmarkTags(mediaId, result.bookmark.tags);
mediaInfo.bookmarkData = result.bookmark;
let { type } = helpers.mediaId.parse(mediaId);
ppixiv.message.show(
wasBookmarked ? "Bookmark edited" : type == "folder" ? "Bookmarked folder" : "Bookmarked"
);
}
static async _localBookmarkRemove(mediaId) {
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, {
full: false
});
if (mediaInfo.bookmarkData == null) {
console.log("Not bookmarked");
return;
}
let result = await LocalAPI.localPostRequest(
\`/api/bookmark/delete/\${mediaId}\`
);
if (!result.success) {
ppixiv.message.show(\`Couldn't remove bookmark: \${result.reason}\`);
return;
}
mediaInfo.bookmarkData = null;
ppixiv.message.show("Bookmark removed");
}
static async bookmarkSetPrivate(mediaId, private_bookmark) {
if (helpers.mediaId.isLocal(mediaId))
return;
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, {
full: false
});
if (!mediaInfo.bookmarkData) {
console.log(\`Illust \${mediaId} wasn't bookmarked\`);
return;
}
let bookmarkId = mediaInfo.bookmarkData.id;
let result = await helpers.pixivRequest.post(
"/ajax/illusts/bookmarks/edit_restrict",
{
bookmarkIds: [bookmarkId],
bookmarkRestrict: private_bookmark ? "private" : "public"
}
);
mediaInfo.bookmarkData = {
id: bookmarkId,
private: private_bookmark
};
ppixiv.message.show(
private_bookmark ? "Bookmarked privately" : "Bookmarked"
);
}
static async addNewBookmarkTag(mediaId) {
console.log("Show tag prompt");
let prompt = new TextPrompt({ title: "New tag:" });
let tags = await prompt.result;
if (tags == null)
return;
tags = tags.split(" ");
tags = tags.filter((value) => {
return value != "";
});
let bookmarkTags = await ppixiv.extraCache.loadBookmarkDetails(mediaId);
let activeTags = [...bookmarkTags];
for (let tag of tags) {
if (activeTags.indexOf(tag) != -1)
continue;
RecentBookmarkTags.updateRecentBookmarkTags([tag]);
activeTags.push(tag);
}
console.log("All tags:", activeTags);
if (helpers.mediaId.isLocal(mediaId))
await Actions._localBookmarkAdd(mediaId, { tags: activeTags });
else
await Actions.bookmarkAdd(mediaId, { tags: activeTags });
}
static async likeImage(mediaId, quiet) {
if (helpers.mediaId.isLocal(mediaId))
return;
let illustId = helpers.mediaId.toIllustIdAndPage(mediaId)[0];
console.log("Clicked like on", mediaId);
if (ppixiv.extraCache.getLikedRecently(mediaId)) {
if (!quiet)
ppixiv.message.show("Already liked this image");
return;
}
let result = await helpers.pixivRequest.post("/ajax/illusts/like", {
illust_id: illustId
});
let wasAlreadyLiked = result.body.is_liked;
ppixiv.extraCache.addLikedRecently(mediaId);
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId);
if (!wasAlreadyLiked && mediaInfo)
mediaInfo.likeCount++;
if (!quiet) {
if (wasAlreadyLiked)
ppixiv.message.show("Already liked this image");
else
ppixiv.message.show("Illustration liked");
}
}
static async follow(userId, followPrivately, { tag = null } = {}) {
if (userId == -1)
return;
let userInfo = await ppixiv.userCache.getUserInfo(userId, { full: true });
if (userInfo.isFollowed) {
console.assert(tag == null);
return await Actions.changeFollowPrivacy(userId, followPrivately);
}
if (followPrivately == null && ppixiv.settings.get("bookmark_privately_by_default"))
followPrivately = true;
let followArgs = {
mode: "add",
type: "user",
user_id: userId,
tag: tag ?? "",
restrict: followPrivately ? 1 : 0,
format: "json"
};
let useRecaptcha = ppixiv.pixivInfo?.pixivTests?.recaptcha_follow_user;
if (useRecaptcha) {
console.log("Requesting recaptcha token for follow");
let token = await getRecaptchaToken("www/follow_user");
if (token == null) {
ppixiv.message.show(
"Couldn't get Recaptcha token for following a user"
);
return;
}
followArgs.recaptcha_enterprise_score_token = token;
}
let result = await helpers.pixivRequest.rpcPost(
"/bookmark_add.php",
followArgs
);
if (result.error) {
ppixiv.message.show(\`Error following user \${userId}: \${result.message}\`);
return;
}
let tagSet = /* @__PURE__ */ new Set();
if (tag != null) {
tagSet.add(tag);
ppixiv.userCache.addCachedUserFollowTags(tag);
}
let info = {
tags: tagSet,
followingPrivately: followPrivately
};
ppixiv.userCache.updateCachedFollowInfo(userId, true, info);
let message = "Followed " + userInfo.name;
if (followPrivately)
message += " privately";
ppixiv.message.show(message);
}
static async changeFollowPrivacy(userId, followPrivately) {
let data = await helpers.pixivRequest.rpcPost(
"/ajax/following/user/restrict_change",
{
user_id: userId,
restrict: followPrivately ? 1 : 0
}
);
if (data.error) {
ppixiv.message.show(\`Error changing follow privacy: \${data.message}\`);
return;
}
let info = ppixiv.userCache.getUserFollowInfoSync(userId);
if (info != null) {
console.log("Updating cached follow privacy");
info.followingPrivately = followPrivately;
ppixiv.userCache.updateCachedFollowInfo(userId, true, info);
}
let userInfo = await ppixiv.userCache.getUserInfo(userId);
let message = \`Now following \${userInfo.name} \${followPrivately ? "privately" : "publically"}\`;
ppixiv.message.show(message);
}
static async changeFollowTags(userId, { tag, add }) {
let data = await helpers.pixivRequest.rpcPost(
add ? "/ajax/following/user/tag_add" : "/ajax/following/user/tag_delete",
{
user_id: userId,
tag
}
);
if (data.error) {
ppixiv.message.show(\`Error editing follow tags: \${data.message}\`);
return;
}
let userInfo = await ppixiv.userCache.getUserInfo(userId);
let message = add ? \`Added the tag "\${tag}" to \${userInfo.name}\` : \`Removed the tag "\${tag}" from \${userInfo.name}\`;
ppixiv.message.show(message);
let followInfo = await ppixiv.userCache.getUserFollowInfo(userId);
if (followInfo == null) {
ppixiv.message.show("Error retrieving follow info to update tags");
return;
}
if (add) {
followInfo.tags.add(tag);
ppixiv.userCache.addCachedUserFollowTags(tag);
} else
followInfo.tags.delete(tag);
ppixiv.userCache.updateCachedFollowInfo(userId, true, followInfo);
}
static async unfollow(userId) {
if (userId == -1)
return;
let result = await helpers.pixivRequest.rpcPost("/rpc_group_setting.php", {
mode: "del",
type: "bookuser",
id: userId
});
let userData = await ppixiv.userCache.getUserInfo(userId);
ppixiv.userCache.updateCachedFollowInfo(userId, false);
ppixiv.message.show("Unfollowed " + userData.name);
}
static async downloadIllust(mediaId, downloadType) {
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId);
let userInfo = await ppixiv.userCache.getUserInfo(mediaInfo.userId);
console.log(\`Download \${mediaId} with type \$[downloadType}\`);
if (downloadType == "MKV") {
new PixivUgoiraDownloader(mediaInfo);
return;
}
if (downloadType !== "image" && downloadType != "ZIP") {
console.error("Unknown download type " + downloadType);
return;
}
let pages = [];
for (let page = 0; page < mediaInfo.mangaPages.length; ++page)
pages.push(page);
const mangaPage = helpers.mediaId.parse(mediaId).page;
if (downloadType === "image")
pages = [mangaPage];
const results = [];
try {
for (const page of pages) {
const pageMediaId = helpers.mediaId.getMediaIdForPage(mediaId, page);
const translatedCanvas = await ppixiv.imageTranslations.getTranslatedImage(pageMediaId);
if (translatedCanvas != null) {
let blob = await helpers.other.canvasToBlob(translatedCanvas, {
type: "image/jpeg",
quality: 0.95
});
let result2 = await blob.arrayBuffer();
result2.extension = "jpg";
results.push(result2);
continue;
}
let url = mediaInfo.mangaPages[page].urls.original;
let result = await downloadPixivImage(url);
result.extension = helpers.strings.getExtension(url);
results.push(result);
}
} catch (e) {
ppixiv.message.show(e.toString());
return;
}
ppixiv.message.hide();
if (pages.length == 1) {
let blob = new Blob([results[0]]);
let ext = results[0].extension;
let isMangaPage = mediaInfo.mangaPages.length > 1 && downloadType == "image";
let filename2 = this.generateFilename(
mediaInfo,
userInfo,
ext,
mangaPage,
isMangaPage
);
helpers.saveBlob(blob, filename2);
return;
}
let filenames = [];
for (let i = 0; i < pages.length; ++i) {
let ext = results[i].extension;
let filename2 = i.toString().padStart(3, "0") + "." + ext;
filenames.push(filename2);
}
let zip2 = new CreateZIP(filenames, results);
let filename = userInfo.name + " - " + mediaInfo.illustId + " - " + mediaInfo.illustTitle + ".zip";
helpers.saveBlob(zip2, filename);
}
static generateFilename(mediaInfo, userInfo, ext, mangaPage, isMangaPage) {
if (ppixiv.settings.get("restore_download_filename")) {
let filename = mediaInfo.illustId + "_p0";
if (isMangaPage)
filename = mediaInfo.illustId + "_p" + (mangaPage + 1);
return filename + "." + ext;
} else {
let filename = userInfo.name + " - " + mediaInfo.illustId;
if (isMangaPage)
filename += " #" + (mangaPage + 1);
return filename + " - " + mediaInfo.illustTitle + "." + ext;
}
}
static isDownloadTypeAvailable(downloadType, mediaInfo) {
if (ppixiv.mobile)
return false;
if (downloadType === "image")
return mediaInfo.illustType !== 2;
if (downloadType === "ZIP")
return mediaInfo.illustType !== 2 && mediaInfo.pageCount > 1;
if (downloadType === "MKV")
return mediaInfo.illustType === 2;
throw \`Unknown download type \${downloadType}\`;
}
static async loadRecentBookmarkTags() {
if (ppixiv.native)
return await LocalAPI.loadRecentBookmarkTags();
const url = \`/ajax/user/\${ppixiv.pixivInfo.userId}/illusts/bookmark/tags\`;
const result = await helpers.pixivRequest.get(url, {});
const bookmarkTags = [];
const addTag = (tag) => {
if (tag.tag === "未分類")
return;
if (bookmarkTags.indexOf(tag.tag) === -1)
bookmarkTags.push(tag.tag);
};
for (const tag of result.body.public)
addTag(tag);
for (const tag of result.body.private)
addTag(tag);
return bookmarkTags;
}
};
/\x2f web/vview/actors/async-lookup.js
var AsyncLookup = class extends Actor {
constructor({
id = null,
onrefresh = async ({}) => {
},
loadWhileNotVisible = false,
...options
}) {
super({ ...options });
this._onrefresh = onrefresh;
this._loadWhileNotVisible = loadWhileNotVisible;
this._id = id;
this._info = {};
helpers.other.defer(() => this.refresh());
}
get id() {
return this._id;
}
set id(value) {
if (this._id == value)
return;
this._id = value;
this.refresh();
}
get info() {
return this._info ?? {};
}
visibilityChanged() {
super.visibilityChanged();
if (!this._loadWhileNotVisible && this.visibleRecursively)
this.refresh();
}
async refresh() {
if (this.hasShutdown)
return;
this._refreshInner();
}
async _refreshInner() {
}
};
/\x2f web/vview/widgets/illust-widgets.js
var GetMediaInfo = class extends AsyncLookup {
constructor({
neededData = "full",
...options
}) {
super({ ...options });
this._neededData = neededData;
if (!(this._neededData instanceof Function))
this._neededData = () => neededData;
this._info = {};
ppixiv.mediaCache.addEventListener(
"mediamodified",
(e) => {
if (e.mediaId == this._id)
this.refresh();
},
this._signal
);
}
get mangaPage() {
let [illustId, page] = helpers.mediaId.toIllustIdAndPage(this._id);
return page;
}
async _refreshInner() {
let mediaId = this._id;
this._info = { mediaId: this._id };
let neededData = this._neededData();
if (this._id != null && neededData != "mediaId") {
let full = neededData == "full";
this._info.mediaInfo = ppixiv.mediaCache.getMediaInfoSync(this._id, {
full
});
if (this._info.mediaInfo == null)
await this._onrefresh(this._info);
this._info.mediaInfo = await ppixiv.mediaCache.getMediaInfo(this._id, {
full
});
}
if (this._id != mediaId)
return;
await this._onrefresh(this._info);
}
};
var IllustWidget = class extends Widget {
constructor(options) {
super(options);
this.getMediaInfo = new GetMediaInfo({
parent: this,
neededData: () => this.neededData,
onrefresh: async (info) => this.refreshInternal(info)
});
}
get neededData() {
return "full";
}
get _mediaId() {
return this.getMediaInfo.id;
}
setMediaId(mediaId) {
this.getMediaInfo.id = mediaId;
}
get mediaId() {
return this.getMediaInfo.id;
}
get mangaPage() {
return this.getMediaInfo.mangaPage;
}
refresh() {
super.refresh();
this.refreshInternal(this.getMediaInfo.info);
}
async refreshInternal({ mediaId, mediaInfo }) {
throw "Not implemented";
}
};
var BookmarkButtonWidget = class extends IllustWidget {
get neededData() {
return "partial";
}
constructor({
template = null,
bookmarkType,
toggleBookmark = true,
bookmarkTagListWidget,
...options
}) {
console.assert(template != null);
super({
template,
...options
});
this.bookmarkType = bookmarkType;
this.toggleBookmark = toggleBookmark;
this._bookmarkTagListWidget = bookmarkTagListWidget;
this.root.addEventListener("click", this.clickedBookmark);
if (bookmarkType === "public")
this.bookmarkCountWidget = new BookmarkCountWidget({
container: this.root
});
}
_fireOnEdited() {
this.dispatchEvent(new Event("bookmarkedited"));
}
set bookmarkTagListWidget(value) {
this._bookmarkTagListWidget = value;
}
get bookmarkTagListWidget() {
return this._bookmarkTagListWidget;
}
refreshInternal({ mediaId, mediaInfo }) {
if (this.bookmarkCountWidget)
this.bookmarkCountWidget.setMediaId(mediaId);
let isLocal2 = helpers.mediaId.isLocal(mediaId);
let isPublic = this.bookmarkType == "public";
helpers.html.setClass(this.root, "has-like-count", isPublic && !isLocal2);
let { type } = helpers.mediaId.parse(mediaId);
if (this.bookmarkType == "private")
this.root.hidden = isLocal2;
let bookmarked = mediaInfo?.bookmarkData != null;
let privateBookmark = this.bookmarkType == "private";
let isOurBookmarkType = mediaInfo?.bookmarkData?.private == privateBookmark;
let willDelete = this.toggleBookmark && isOurBookmarkType;
if (this.bookmarkType == "delete")
isOurBookmarkType = willDelete = bookmarked;
helpers.html.setClass(this.root, "enabled", mediaInfo != null);
helpers.html.setClass(this.root, "bookmarked", isOurBookmarkType);
helpers.html.setClass(this.root, "will-delete", willDelete);
this.root.dataset.popup = mediaInfo == null ? "" : !bookmarked && this.bookmarkType == "folder" ? "Bookmark folder" : !bookmarked && this.bookmarkType == "private" ? "Bookmark privately" : !bookmarked && this.bookmarkType == "public" && type == "folder" ? "Bookmark folder" : !bookmarked && this.bookmarkType == "public" ? "Bookmark image" : willDelete ? "Remove bookmark" : "Change bookmark to " + this.bookmarkType;
}
clickedBookmark = async (e) => {
let a = e.target.closest(".button-bookmark");
if (a == null)
return;
e.preventDefault();
e.stopPropagation();
let tagList = null;
if (this._bookmarkTagListWidget && this._bookmarkTagListWidget.visibleRecursively)
tagList = this._bookmarkTagListWidget.selectedTags;
if (this._bookmarkTagListWidget)
this._bookmarkTagListWidget.deactivate();
this._fireOnEdited();
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(this._mediaId, {
full: false
});
let privateBookmark = this.bookmarkType == "private";
let deleteBookmark = this.toggleBookmark && mediaInfo.bookmarkData?.private == privateBookmark;
if (this.bookmarkType == "delete")
deleteBookmark = true;
if (deleteBookmark) {
if (!mediaInfo.bookmarkData)
return;
if (ppixiv.mobile) {
let result = await new ConfirmPrompt({ header: "Remove bookmark?" }).result;
if (!result)
return;
}
let mediaId = this._mediaId;
await Actions.bookmarkRemove(this._mediaId);
if (mediaId != this._mediaId)
return;
if (this._bookmarkTagListWidget)
this._bookmarkTagListWidget.deactivate();
this._fireOnEdited();
return;
}
await Actions.bookmarkAdd(this._mediaId, {
private: privateBookmark,
tags: tagList
});
};
};
var BookmarkCountWidget = class extends IllustWidget {
constructor({ ...options }) {
super({
...options,
template: \`
<div class=count></div>
\`
});
}
refreshInternal({ mediaId, mediaInfo }) {
let text = "";
if (!helpers.mediaId.isLocal(mediaId))
text = mediaInfo?.bookmarkCount ?? "---";
this.root.textContent = text;
}
};
var LikeButtonWidget = class extends IllustWidget {
get neededData() {
return "mediaId";
}
constructor({
template = null,
...options
}) {
console.assert(template != null), super({
template,
...options
});
this.root.addEventListener("click", this.clickedLike);
this.likeCount = new LikeCountWidget({
container: this.root
});
}
async refreshInternal({ mediaId }) {
this.likeCount.setMediaId(mediaId);
this.root.closest(".button-container").hidden = helpers.mediaId.isLocal(mediaId);
let likedRecently = mediaId != null ? ppixiv.extraCache.getLikedRecently(mediaId) : false;
helpers.html.setClass(this.root, "liked", likedRecently);
helpers.html.setClass(this.root, "enabled", !likedRecently);
this.root.dataset.popup = this._mediaId == null ? "" : likedRecently ? "Image liked" : "Like image";
}
clickedLike = (e) => {
e.preventDefault();
e.stopPropagation();
if (this._mediaId != null)
Actions.likeImage(this._mediaId);
};
};
var LikeCountWidget = class extends IllustWidget {
constructor({ ...options }) {
super({
...options,
template: \`
<div class=count></div>
\`
});
}
async refreshInternal({ mediaId, mediaInfo }) {
let text = "";
if (!helpers.mediaId.isLocal(mediaId))
text = mediaInfo?.likeCount ?? "---";
this.root.textContent = text;
}
};
/\x2f web/vview/widgets/dropdown.js
var DropdownBoxOpener = class extends Actor {
constructor({
button,
shouldCloseForClick = (e) => true,
createDropdown = null,
onVisibilityChanged = () => {
},
asDialog = () => false,
clickToOpen = false,
parent = null,
...options
}) {
parent ??= Widget.fromNode(button);
super({
parent,
...options
});
this.button = button;
this.shouldCloseForClick = shouldCloseForClick;
this.onVisibilityChanged = onVisibilityChanged;
this.createDropdown = createDropdown;
if (asDialog instanceof Function)
this.asDialog = asDialog;
else
this.asDialog = () => asDialog;
this._dropdown = null;
this._visible = false;
this._boxWidth = 0;
if (clickToOpen)
this.button.addEventListener(
"click",
(e) => this.visible = !this.visible,
this._signal
);
}
onwindowresize = (e) => {
this._alignToButton();
};
visibilityChanged() {
super.visibilityChanged();
if (!this.visibleRecursively)
this.visible = false;
}
get visible() {
return this._visible;
}
get dropdown() {
return this._dropdown;
}
set visible(value) {
if (this._visible == value)
return;
this._visible = value;
let topUiBox = this.parent.closest(".top-ui-box");
if (topUiBox)
helpers.html.setClass(topUiBox, "force-open", value);
OpenWidgets.singleton.set(this, value);
if (value) {
let asDialog = this.asDialog();
if (window.ppixiv?.mobile) {
for (let node of this.ancestors()) {
if (node instanceof DialogWidget) {
console.log(
"Opening dropdown as a dialog because we're inside another dialog:",
node
);
asDialog = true;
break;
}
}
}
let container = document.body;
let parent = this;
this._dropdownDialog = null;
if (asDialog) {
parent = this._dropdownDialog = new DialogWidget({
parent: this,
template: \`<div></div>\`
});
this._dropdownDialog.shutdownSignal.addEventListener("abort", (e) => {
if (e.target != this._dropdownDialog?.shutdownSignal)
return;
console.log("Dialog dropdown closed");
this._dropdownDialog = null;
this._dropdown = null;
this.visible = false;
});
container = this._dropdownDialog.querySelector(".scroll");
}
this._dropdown = this.createDropdown({ container, parent });
if (this._dropdown == null) {
this._visible = false;
if (this._dropdownDialog) {
this._dropdownDialog.shutdown();
this._dropdownDialog = null;
}
return;
}
if (!asDialog) {
this._dropdown.root.classList.add("dropdown-box");
this.listener = new ClickOutsideListener(
[this.button, this._dropdown],
(target, { event }) => {
if (!this.shouldCloseForClick(event))
return;
this.visible = false;
}
);
this._resizeObserver = new ResizeObserver(() => {
if (this._boxWidth == this._dropdown.root.offsetWidth)
return;
this._boxWidth = this._dropdown.root.offsetWidth;
this._alignToButton();
});
this._resizeObserver.observe(this._dropdown.root);
window.addEventListener("resize", this.onwindowresize, this._signal);
this._alignToButton();
}
if (this.closeOnClickInside)
this._dropdown.root.addEventListener("click", this.boxClicked);
} else {
if (this._dropdownDialog) {
this._dropdownDialog.visible = false;
this._dropdownDialog = null;
this._dropdown = null;
return;
}
this._cleanup();
if (this._dropdown) {
this._dropdown.shutdown();
this._dropdown = null;
}
}
this.onVisibilityChanged(this);
}
_cleanup() {
this.visible = false;
if (this._resizeObserver) {
this._resizeObserver.disconnect();
this._resizeObserver = null;
}
if (this.listener) {
this.listener.shutdown();
this.listener = null;
}
window.removeEventListener("resize", this.onwindowresize);
OpenWidgets.singleton.set(this, false);
}
_alignToButton() {
if (!this.visible)
return;
if (this._dropdownDialog)
return;
let horizontalPadding = 4, verticalPadding = 8;
let buttonParent = this.button;
this._dropdown.root.style.zIndex = 1;
while (buttonParent) {
let { zIndex } = getComputedStyle(buttonParent);
if (zIndex != "auto") {
zIndex = parseInt(zIndex);
this._dropdown.root.style.zIndex = zIndex + 1;
break;
}
buttonParent = buttonParent.offsetParent;
}
let {
left: buttonX,
top: buttonY,
height: boxHeight
} = this.button.getBoundingClientRect();
let x = buttonX - horizontalPadding;
let padding = window.ppixiv?.mobile ? 0 : 4;
let rightEdge = x + this._boxWidth;
x -= Math.max(rightEdge - (window.innerWidth - padding), 0);
x = Math.max(x, 0);
let y = buttonY;
this._dropdown.root.style.left = \`\${x}px\`;
if (y < window.innerHeight / 2) {
y += boxHeight + verticalPadding;
this._dropdown.root.style.top = \`\${y}px\`;
this._dropdown.root.style.bottom = "";
let height = window.innerHeight - y - padding;
this._dropdown.root.style.maxHeight = \`\${height}px\`;
} else {
y -= verticalPadding;
this._dropdown.root.style.top = "";
this._dropdown.root.style.bottom = \`calc(100% - \${y}px)\`;
let height = y - padding;
this._dropdown.root.style.maxHeight = \`\${height}px\`;
}
}
shutdown() {
this._cleanup();
super.shutdown();
}
get closeOnClickInside() {
return false;
}
};
var DropdownMenuOpener = class extends DropdownBoxOpener {
constructor({
createDropdown = null,
...options
}) {
super({
createDropdown: (...args) => {
let widget = createDropdown(...args);
widget.root.classList.add("popup-menu-box");
return widget;
},
...options
});
this.button.addEventListener(
"click",
(e) => this._buttonClicked(e),
this._signal
);
this.setButtonPopupHighlight();
}
get closeOnClickInside() {
return true;
}
boxClicked = (e) => {
if (e.target.closest(".keep-menu-open"))
return;
this.visible = false;
};
_buttonClicked(e) {
e.preventDefault();
e.stopPropagation();
this.visible = !this.visible;
}
setButtonPopupHighlight() {
let tempBox = this.createDropdown({ container: document.body });
DropdownMenuOpener.setActivePopupHighlightFrom(this.button, tempBox.root);
tempBox.shutdown();
}
static setActivePopupHighlightFrom(button, box) {
let selectedItem = box.querySelector(".selected");
let selectedDefault = selectedItem == null || selectedItem.dataset["default"];
let itemHasDefault = box.querySelector("[data-default]") != null;
if (itemHasDefault && selectedItem == null)
selectedDefault = false;
helpers.html.setClass(button, "selected", !selectedDefault);
helpers.html.setClass(box, "selected", !selectedDefault);
if (!selectedDefault) {
let text = selectedItem?.dataset?.shortLabel;
let selectedLabel = selectedItem?.querySelector(".label")?.innerText;
let label = button.querySelector(".label");
label.innerText = text ?? selectedLabel ?? "Other";
}
}
};
/\x2f web/vview/widgets/bookmark-tag-list.js
var BookmarkTagListWidget = class extends IllustWidget {
get neededData() {
return "mediaId";
}
constructor({ ...options }) {
super({
...options,
template: \`
<div class="bookmark-tag-list">
<div class="tag-list vertical-list">
</div>
</div>
\`
});
this.displayingMediaId = null;
this.root.addEventListener("click", this._clickedBookmarkTag, true);
this.deactivated = false;
ppixiv.settings.addEventListener(
"recent-bookmark-tags",
this.refresh.bind(this)
);
}
deactivate() {
this.deactivated = true;
}
shutdown() {
this.visible = false;
super.shutdown();
}
get selectedTags() {
let tagList = [];
let bookmarkTags = this.root;
for (let entry of bookmarkTags.querySelectorAll(
".popup-bookmark-tag-entry"
)) {
if (!entry.classList.contains("selected"))
continue;
tagList.push(entry.dataset.tag);
}
return tagList;
}
setMediaId(mediaId) {
if (mediaId == null)
this.saveCurrentTags();
super.setMediaId(mediaId);
}
async visibilityChanged() {
if (this.visible) {
if (this.deactivated)
console.info("reactivating tag list widget");
this.deactivated = false;
await this.refresh();
} else {
this.saveCurrentTags();
this._clearTagList();
this.displayingMediaId = null;
}
super.visibilityChanged();
}
_clearTagList() {
let bookmarkTags = this.root.querySelector(".tag-list");
for (let element of [...bookmarkTags.children]) {
if (element.classList.contains("dynamic") || element.classList.contains("loading"))
element.remove();
}
}
async refreshInternal({ mediaId }) {
if (this.deactivated)
return;
if (!this.visible)
mediaId = null;
let oldSelectedTags = this.displayingMediaId == mediaId ? this.selectedTags : [];
let bookmarkTags = this.root.querySelector(".tag-list");
if (mediaId != this.displayingMediaId) {
this._clearTagList();
this.displayingMediaId = null;
}
if (mediaId == null)
return;
if (mediaId != this.displayingMediaId) {
let entry = document.createElement("span");
entry.classList.add("loading");
bookmarkTags.appendChild(entry);
entry.innerText = "Loading...";
}
let activeTags = await ppixiv.extraCache.loadBookmarkDetails(mediaId);
this.displayingMediaId = mediaId;
this._clearTagList();
for (let tag of oldSelectedTags) {
if (activeTags.indexOf(tag) == -1)
activeTags.push(tag);
}
let shownTags = [];
let recentBookmarkTags = [...RecentBookmarkTags.getRecentBookmarkTags()];
for (let tag of recentBookmarkTags)
if (shownTags.indexOf(tag) == -1)
shownTags.push(tag);
for (let tag of activeTags)
if (shownTags.indexOf(tag) == -1)
shownTags.push(tag);
shownTags.sort(
(lhs, rhs) => lhs.toLowerCase().localeCompare(rhs.toLowerCase())
);
let createEntry = (tag, { classes = [], icon } = {}) => {
let entry = this.createTemplate({
name: "tag-entry",
html: \`
<div class="popup-bookmark-tag-entry dynamic">
<span class=tag-name></span>
</div>
\`
});
for (let cls of classes)
entry.classList.add(cls);
entry.querySelector(".tag-name").innerText = tag;
if (icon)
entry.querySelector(".tag-name").insertAdjacentElement("afterbegin", icon);
bookmarkTags.appendChild(entry);
return entry;
};
let addButton = createEntry("Add", {
icon: helpers.createIcon("add", { asElement: true }),
classes: ["add-button"]
});
addButton.addEventListener(
"click",
() => Actions.addNewBookmarkTag(this._mediaId)
);
for (let tag of shownTags) {
let entry = createEntry(tag, {
classes: ["tag-toggle"]
});
entry.dataset.tag = tag;
let active = activeTags.indexOf(tag) != -1;
helpers.html.setClass(entry, "selected", active);
}
let syncButton = createEntry("Refresh", {
icon: helpers.createIcon("refresh", { asElement: true }),
classes: ["refresh-button"]
});
syncButton.addEventListener("click", async (e) => {
let bookmarkTags2 = await Actions.loadRecentBookmarkTags();
RecentBookmarkTags.setRecentBookmarkTags(bookmarkTags2);
this.refreshInternal({ mediaId: this.mediaId });
});
}
async saveCurrentTags() {
if (this.deactivated)
return;
let mediaId = this._mediaId;
let newTags = this.selectedTags;
if (mediaId == null)
return;
if (mediaId != this.displayingMediaId)
return;
let oldTags = await ppixiv.extraCache.loadBookmarkDetails(mediaId);
let equal = newTags.length == oldTags.length;
for (let tag of newTags) {
if (oldTags.indexOf(tag) == -1)
equal = false;
}
if (equal)
return;
console.log(
\`Tag list closing and tags have changed: "\${oldTags.join(",")}" -> "\${newTags.join(",")}"\`
);
await Actions.bookmarkAdd(this._mediaId, {
tags: newTags
});
}
_clickedBookmarkTag = async (e) => {
if (this.deactivated)
return;
let a = e.target.closest(".tag-toggle");
if (a == null)
return;
e.preventDefault();
e.stopPropagation();
helpers.html.setClass(a, "selected", !a.classList.contains("selected"));
};
};
var BookmarkTagListDropdownWidget = class extends BookmarkTagListWidget {
constructor({ mediaId, bookmarkButtons, ...options }) {
super({
classes: ["popup-bookmark-tag-dropdown"],
...options
});
this.root.classList.add("popup-bookmark-tag-dropdown");
this.bookmarkButtons = bookmarkButtons;
this.setMediaId(mediaId);
for (let bookmarkButton of this.bookmarkButtons)
bookmarkButton.bookmarkTagListWidget = this;
}
async refreshInternal({ mediaId }) {
if (mediaId == null)
this.visible = false;
await super.refreshInternal({ mediaId });
}
visibilityChanged() {
super.visibilityChanged();
if (!this.visibleRecursively)
this.visible = false;
}
shutdown() {
super.shutdown();
for (let bookmarkButton of this.bookmarkButtons) {
if (bookmarkButton.bookmarkTagListWidget == this)
bookmarkButton.bookmarkTagListWidget = null;
}
}
};
var BookmarkTagDropdownOpener = class extends Actor {
constructor({
bookmarkTagsButton,
bookmarkButtons,
onVisibilityChanged,
...options
}) {
super({ ...options });
this.bookmarkTagsButton = bookmarkTagsButton;
this.bookmarkButtons = bookmarkButtons;
this._mediaId = null;
this._opener = new DropdownBoxOpener({
button: bookmarkTagsButton,
onVisibilityChanged,
createDropdown: this._createBox,
shouldCloseForClick: (e) => {
for (let button of this.bookmarkButtons) {
if (helpers.html.isAbove(button.root, e.target))
return false;
}
return true;
}
});
bookmarkTagsButton.addEventListener("click", (e) => {
this._opener.visible = !this._opener.visible;
});
for (let button of this.bookmarkButtons) {
button.addEventListener(
"bookmarkedited",
() => {
this._opener.visible = false;
},
this._signal
);
}
}
setMediaId(mediaId) {
if (this._mediaId == mediaId)
return;
this._mediaId = mediaId;
helpers.html.setClass(this.bookmarkTagsButton, "enabled", mediaId != null);
this._opener.visible = false;
}
_createBox = ({ ...options }) => {
if (this._mediaId == null)
return;
return new BookmarkTagListDropdownWidget({
...options,
parent: this,
mediaId: this._mediaId,
bookmarkButtons: this.bookmarkButtons
});
};
set visible(value) {
this._opener.visible = value;
}
get visible() {
return this._opener.visible;
}
};
/\x2f web/vview/widgets/user-widgets.js
var GetUserInfo = class extends AsyncLookup {
constructor({
neededData = "full",
...options
}) {
super({ ...options });
this._neededData = neededData;
if (!(this._neededData instanceof Function))
this._neededData = () => neededData;
ppixiv.userCache.addEventListener(
"usermodified",
(e) => {
if (e.userId == this._id)
this.refresh();
},
this._signal
);
}
async _refreshInner() {
if (this.hasShutdown)
return;
let userId = this._id;
let info = { userId: this._id };
let neededData = this._neededData();
if (this._id != null && neededData != "userId") {
let full = neededData == "full";
info.userInfo = ppixiv.userCache.getUserInfoSync(this._id, { full });
if (info.userInfo == null) {
await this._onrefresh(info);
if (!this._loadWhileNotVisible && !this.actuallyVisibleRecursively)
return;
info.userInfo = await ppixiv.userCache.getUserInfo(this._id, { full });
}
}
if (this._id != userId)
return;
await this._onrefresh(info);
}
};
var GetUserIdFromMediaId = class extends AsyncLookup {
constructor({ ...options }) {
super({ ...options });
this._id = null;
}
async _refreshInner() {
let mediaId = this._id;
this._info = {};
if (this._id != null) {
let { type, id } = helpers.mediaId.parse(mediaId);
if (type == "user")
this._info.userId = id;
else {
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(this._id, {
full: false
});
this._info.userId = mediaInfo?.userId;
if (this._info.userId == null) {
await this._onrefresh(this._info);
if (!this._loadWhileNotVisible && !this.actuallyVisibleRecursively)
return;
mediaInfo = await ppixiv.mediaCache.getMediaInfo(this._id, {
full: false
});
this._info.userId = mediaInfo?.userId;
}
}
}
if (this._id !== mediaId)
return;
await this._onrefresh(this._info);
}
};
var AvatarWidget = class extends Widget {
constructor({
dropdownVisibilityChanged = () => {
},
clickAction = "dropdown",
...options
} = {}) {
super({
...options,
template: \`
<a href=# class=avatar-widget data-scroll-to-top>
<canvas class=avatar></canvas>
<div class=follow-icon>
</div>
</a>
\`
});
this.options = options;
if (clickAction !== "dropdown" && clickAction !== "author")
throw new Error(\`Invalid avatar widget mode: \${clickAction}\`);
this.getUserInfo = new GetUserInfo({
parent: this,
onrefresh: (args) => this.onrefresh(args)
});
const avatarElement = this.root.querySelector(".avatar");
const avatarLink = this.root;
this.followDropdownOpener = new DropdownBoxOpener({
button: avatarLink,
onVisibilityChanged: dropdownVisibilityChanged,
asDialog: ppixiv.mobile,
createDropdown: ({ ...options2 }) => {
return new FollowWidget({
...options2,
userId: this.userId,
close: () => {
this.followDropdownOpener.visible = false;
}
});
}
});
avatarLink.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
if (clickAction === "dropdown")
this.followDropdownOpener.visible = !this.followDropdownOpener.visible;
else if (clickAction === "author") {
const args = new helpers.args(\`/users/\${this.userId}#ppixiv\`);
helpers.navigate(args, { scrollToTop: true });
}
});
avatarLink.addEventListener("dblclick", (e) => {
e.preventDefault();
e.stopPropagation();
const args = new helpers.args(\`/users/\${this.userId}/artworks#ppixiv\`);
helpers.navigate(args, { scrollToTop: true });
});
this.img = document.createElement("img");
this._baseFilter = new ImageCanvasFilter(this.img, avatarElement);
this.root.dataset.mode = this.options.mode;
new CreepyEyeWidget({
container: this.root.querySelector(".follow-icon"),
pointerTarget: this.root
});
}
visibilityChanged() {
super.visibilityChanged();
this.refresh();
}
get userDropdownWidget() {
return this.followDropdownOpener.dropdown;
}
get userId() {
return this.getUserInfo.id;
}
async setUserId(userId) {
if (this.getUserInfo.id !== userId && this.followDropdownOpener)
this.followDropdownOpener.visible = false;
this.getUserInfo.id = userId;
this.refresh();
}
onrefresh({ userId, userInfo }) {
if (userId == null || userId === -1) {
this.img.src = helpers.other.blankImage;
return;
}
const cachedProfileUrl = ppixiv.mediaCache.userProfileUrls[userId];
this.img.src = cachedProfileUrl ?? userInfo?.imageBig ?? helpers.other.blankImage;
this.root.href = \`/users/\${userId}/artworks#ppixiv\`;
if (this.options.mode === "dropdown")
this.root.querySelector(".avatar").classList.remove("popup");
helpers.html.setClass(this.root, "followed", false);
this.root.querySelector(".avatar").dataset.popup = "";
this.root.querySelector(".follow-icon").hidden = !(userInfo?.isFollowed ?? false);
this.root.querySelector(".avatar").dataset.popup = userInfo?.name ?? "";
}
};
var ImageCanvasFilter = class {
constructor(img, canvas, baseFilter, overlay) {
this.img = img;
this.canvas = canvas;
this._baseFilter = baseFilter || "";
this.overlay = overlay;
this.ctx = this.canvas.getContext("2d");
this.img.addEventListener("load", this._updateCanvas);
this.observer = new MutationObserver((mutations) => {
for (let mutation of mutations) {
if (mutation.type == "attributes") {
if (mutation.attributeName == "src") {
this._updateCanvas();
}
}
}
});
this.observer.observe(this.img, { attributes: true });
this._updateCanvas();
}
clear() {
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this._currentUrl = helpers.other.blankImage;
}
_updateCanvas = () => {
let currentUrl = this.img.src;
if (!this.img.complete)
currentUrl = helpers.other.blankImage;
if (currentUrl == this._currentUrl)
return;
helpers.html.setClass(this.canvas, "loaded", false);
this.canvas.width = this.img.naturalWidth;
this.canvas.height = this.img.naturalHeight;
this.clear();
this._currentUrl = currentUrl;
if (currentUrl == helpers.other.blankImage)
return;
this.ctx.save();
this.ctx.filter = this._baseFilter;
this.ctx.drawImage(this.img, 0, 0);
this.ctx.restore();
this.ctx.save();
if (this.overlay)
this.overlay(this.ctx, this.img);
this.ctx.restore();
this.ctx.globalCompositeOperation = "destination-over";
this.ctx.drawImage(this.img, 0, 0);
helpers.html.setClass(this.canvas, "loaded", true);
};
};
var CreepyEyeWidget = class extends Widget {
constructor({ pointerTarget, ...options } = {}) {
super({
...options,
template: \`
<ppixiv-inline src="resources/eye-icon.svg"></ppixiv-inline>
\`
});
pointerTarget.addEventListener("mouseover", this.onevent, {
capture: true,
...this._signal
});
pointerTarget.addEventListener("mouseout", this.onevent, {
capture: true,
...this._signal
});
pointerTarget.addEventListener("pointermove", this.onevent, {
capture: true,
...this._signal
});
}
onevent = (e) => {
const bounds = this.root.getBoundingClientRect();
const expandRatio = 1.5;
const width = bounds.width;
const height = bounds.height;
this.hover = bounds.left - width * expandRatio <= e.clientX && e.clientX <= bounds.right + width * expandRatio && bounds.top - height * expandRatio <= e.clientY && e.clientY <= bounds.bottom + height * expandRatio;
if (e.type === "mouseover")
this.hover = true;
if (e.type === "mouseout")
this.hover = false;
const eyeMiddle = this.root.querySelector(".middle");
if (!this.hover) {
eyeMiddle.style.transform = "";
return;
}
const mouse = [e.clientX, e.clientY];
const eye = [bounds.x + bounds.width / 2, bounds.y + bounds.height / 2];
const vectorLength = (vec) => Math.sqrt(vec[0] * vec[0] + vec[1] * vec[1]);
const normalizeVector = (vec) => {
const length = vectorLength(vec);
if (length < 1e-4)
return [0, 0];
return [vec[0] / length, vec[1] / length];
};
let pos = [mouse[0] - eye[0], mouse[1] - eye[1]];
pos = normalizeVector(pos);
if (Math.abs(pos[0]) < 0.5) {
const negative = pos[0] < 0;
pos[0] = 0.5;
if (negative)
pos[0] *= -1;
}
pos[0] *= 3;
pos[1] *= 6;
eyeMiddle.style.transform = "translate(" + pos[0] + "px, " + pos[1] + "px)";
};
};
var FollowWidget = class extends Widget {
constructor({
userId = null,
close = () => {
},
...options
}) {
super({
...options,
template: \`
<div class="follow-widget vertical-list">
\${helpers.createBoxLink({ label: "", icon: "mat:palette", classes: ["view-posts"], dataset: { scrollToTop: true } })}
<!-- Buttons for following, unfollowing, and changing privacy. -->
\${helpers.createBoxLink({ label: "Follow", icon: "public", classes: ["follow-button-public"] })}
\${helpers.createBoxLink({ label: "Follow privately", icon: "lock", classes: ["follow-button-private"] })}
\${helpers.createBoxLink({ label: "Unfollow", icon: "delete", classes: ["unfollow-button"] })}
\${helpers.createBoxLink({
label: "Change follow to private",
icon: "mat:hourglass_full",
classes: ["follow-placeholder", "disabled"]
})}
<!-- Buttons for toggling a follow between public and private. This is separate
from the buttons above, since it comes after to make sure that the unfollow
button is above the toggle buttons. -->
\${helpers.createBoxLink({ label: "Change follow to public", icon: "public", classes: ["toggle-follow-button-public"] })}
\${helpers.createBoxLink({ label: "Change follow to private", icon: "lock", classes: ["toggle-follow-button-private"] })}
\${helpers.createBoxLink({ label: "Follow tags", icon: "mat:bookmark", classes: ["follow-tags", "premium-only"] })}
</div>
\`
});
this.userId = userId;
this.close = close;
this.data = {};
this.viewPosts = this.querySelector(".view-posts");
this.root.querySelector(".follow-button-public").addEventListener("click", (e) => this._clickedFollow(false));
this.root.querySelector(".follow-button-private").addEventListener("click", (e) => this._clickedFollow(true));
this.root.querySelector(".toggle-follow-button-public").addEventListener("click", (e) => this._clickedFollow(false));
this.root.querySelector(".toggle-follow-button-private").addEventListener("click", (e) => this._clickedFollow(true));
this.root.querySelector(".unfollow-button").addEventListener("click", (e) => this._clickedUnfollow());
ppixiv.userCache.addEventListener(
"usermodified",
this._userChanged,
this._signal
);
ppixiv.muting.addEventListener(
"mutes-changed",
() => this.refresh(),
this._signal
);
this.followTagDropdownOpener = new DropdownBoxOpener({
button: this.querySelector(".follow-tags"),
clickToOpen: true,
createDropdown: ({ ...options2 }) => {
return new FollowTagWidget({
...options2,
userId: this.userId
});
}
});
this.loadUser();
}
_userChanged = ({ userId }) => {
if (!this.visible || userId != this.userId)
return;
this.loadUser();
};
async loadUser() {
if (!this.visible)
return;
this.data = {};
this.refresh();
let userInfo = ppixiv.userCache.getUserInfoSync(this.userId);
if (userInfo)
this.data.userInfo = userInfo;
else
userInfo = ppixiv.userCache.getUserInfo(this.userId);
let userProfile = ppixiv.userCache.getUserProfileSync(this.userId);
if (userProfile)
this.data.userProfile = userProfile;
else
userProfile = ppixiv.userCache.getUserProfile(this.userId);
this.refresh();
if (this.data.userInfo?.isFollowed)
this._requestFollowInfo();
if (userInfo)
this.data.userInfo = await userInfo;
if (userProfile)
this.data.userProfile = await userProfile;
this.refresh();
if (this.data.userInfo?.isFollowed)
this._requestFollowInfo();
this._requestBoothInfo();
}
async _requestFollowInfo() {
let followInfo = await ppixiv.userCache.getUserFollowInfo(this.userId);
this.data.followingPrivately = followInfo?.followingPrivately;
this.refresh();
}
async _requestBoothInfo() {
this.data.boothUrl = await ppixiv.userCache.getUserBoothUrl(this.userId);
this.refresh();
}
refresh() {
this.viewPosts.href = \`/users/\${this.userId}#ppixiv\`;
let { followingPrivately = null, ...otherUserInfo } = this.data;
if (!this.visible)
return;
let userInfo = ppixiv.userCache.getUserInfoSync(this.userId);
this.viewPosts.querySelector(".label").textContent = userInfo?.name ?? "";
let infoLinksContainer = this.root;
for (let link of this.querySelectorAll(".info-link, .separator"))
link.remove();
if (userInfo) {
let links = this._getInfoLinksForUser({ userInfo, ...otherUserInfo });
links = this._filterLinks(links);
for (let { url, label, type, icon, disabled } of links) {
if (type == "separator") {
let separator = document.createElement("div");
separator.classList.add("separator");
infoLinksContainer.appendChild(separator);
continue;
}
let button = helpers.createBoxLink({
asElement: true,
label,
icon,
link: url,
classes: ["info-link"]
});
if (disabled)
button.classList.add("disabled");
if (type == "mute") {
button.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
this._clickedMute();
});
}
infoLinksContainer.appendChild(button);
}
}
this.root.querySelector(".follow-button-public").hidden = true;
this.root.querySelector(".follow-button-private").hidden = true;
this.root.querySelector(".toggle-follow-button-public").hidden = true;
this.root.querySelector(".toggle-follow-button-private").hidden = true;
this.root.querySelector(".unfollow-button").hidden = true;
this.root.querySelector(".follow-placeholder").hidden = true;
let following = userInfo?.isFollowed;
if (following != null) {
if (following) {
if (followingPrivately != null) {
this.root.querySelector(".toggle-follow-button-public").hidden = !followingPrivately;
this.root.querySelector(".toggle-follow-button-private").hidden = followingPrivately;
} else {
this.root.querySelector(".follow-placeholder").hidden = false;
}
this.root.querySelector(".unfollow-button").hidden = false;
} else {
this.root.querySelector(".follow-button-public").hidden = false;
this.root.querySelector(".follow-button-private").hidden = false;
}
}
for (let element of this.root.querySelectorAll(".follow-tag"))
element.remove();
}
async _clickedFollow(followPrivately) {
this.close();
await Actions.follow(this.userId, followPrivately);
this._requestFollowInfo();
}
async _clickedUnfollow() {
this.close();
if (ppixiv.mobile) {
let userInfo = ppixiv.userCache.getUserInfoSync(this.userId);
let result = await new ConfirmPrompt({
header: userInfo ? \`Unfollow \${userInfo.name}?\` : "Unfollow?"
}).result;
if (!result)
return;
}
await Actions.unfollow(this.userId);
}
async _clickedMute() {
if (ppixiv.muting.isUserIdMuted(this.userId))
ppixiv.muting.unmuteUserId(this.userId);
else
await ppixiv.muting.addMute(this.userId, null, { type: "user" });
}
_getInfoLinksForUser({ userInfo, userProfile, boothUrl } = {}) {
if (userInfo == null)
return [];
let extraLinks = [];
extraLinks.push({
url: new URL(
\`/discovery/users#ppixiv?user_id=\${userInfo.userId}\`,
ppixiv.plocation
),
type: "similar-artists",
label: "Similar artists"
});
extraLinks.push({
url: new URL(
\`/users/\${userInfo.userId}/following#ppixiv\`,
ppixiv.plocation
),
type: "following-link",
label: \`View followed users\`
});
extraLinks.push({
url: new URL(
\`/users/\${userInfo.userId}/bookmarks/artworks#ppixiv\`,
ppixiv.plocation
),
type: "bookmarks-link",
label: \`View bookmarks\`
});
extraLinks.push({
url: new URL(
\`/messages.php?receiver_id=\${userInfo.userId}\`,
ppixiv.plocation
),
type: "contact-link",
label: "Send a message"
});
let muted = ppixiv.muting.isUserIdMuted(userInfo.userId);
extraLinks.unshift({
type: "mute",
label: \`\${muted ? "Unmute" : "Mute"} this user\`,
icon: "mat:block"
});
let acceptRequest = userProfile?.body?.request?.showRequestTab;
if (acceptRequest) {
extraLinks.push({
url: new URL(
\`/users/\${this.userId}/request#no-ppixiv\`,
ppixiv.plocation
),
type: "request",
label: "Accepting requests"
});
}
extraLinks.push({ type: "separator" });
let knownSocialKeys = {
circlems: {
label: "Circle.ms"
}
};
let social = userInfo?.social ?? [];
for (let [key, { url }] of Object.entries(social)) {
let data = knownSocialKeys[key] ?? {};
data.label ??= helpers.strings.titleCase(key);
extraLinks.push({ url, ...data });
}
let webpageUrl = userInfo?.webpage;
if (webpageUrl != null) {
extraLinks.push({
url: webpageUrl,
label: "Webpage",
type: this._findLinkImageType(webpageUrl) ?? "webpage-link"
});
}
let div = document.createElement("div");
div.innerHTML = userInfo.commentHtml;
for (let link of div.querySelectorAll("a")) {
let url = helpers.pixiv.fixPixivLink(link.href);
try {
url = new URL(url);
} catch (e) {
console.log("Couldn't parse profile URL:", url);
continue;
}
let label = url.hostname;
let imageType = this._findLinkImageType(url);
if (imageType == "booth")
label = "Booth";
else if (imageType == "fanbox")
label = "Fanbox";
else if (label.startsWith("www."))
label = label.substr(4);
extraLinks.push({
url,
label
});
}
let pickups = userProfile?.body?.pickup ?? [];
for (let pickup of pickups) {
if (pickup.type != "fanbox")
continue;
let url = new URL(pickup.contentUrl);
url.search = "";
extraLinks.push({ url, type: "fanbox", label: "Fanbox" });
break;
}
if (boothUrl)
extraLinks.push({ url: boothUrl, label: "Booth" });
else if (userProfile?.body?.externalSiteWorksStatus?.booth)
extraLinks.push({
url: window.location,
label: "Booth",
icon: "mat:hourglass_full",
disabled: true
});
window.vviewHooks?.addUserLinks?.({ extraLinks, userInfo, userProfile });
return extraLinks;
}
_filterLinks(extraLinks) {
let linkTypes = {
["default-icon"]: "ppixiv:link",
["shopping-cart"]: "mat:shopping_cart",
["webpage-link"]: "mat:home",
["commercial"]: "mat:paid",
["posts"]: "mat:palette",
["twitter"]: "ppixiv:twitter",
["fanbox"]: "mat:paid",
["request"]: "mat:paid",
["booth"]: "mat:shopping_cart",
["twitch"]: "ppixiv:twitch",
["contact-link"]: "mat:mail",
["following-link"]: "mat:visibility",
["bookmarks-link"]: "mat:star",
["similar-artists"]: "ppixiv:suggestions",
["mute"]: "block"
};
let filteredLinks = [];
let seenLinks = {};
let seenTypes = {};
for (let { type, url, label, ...other } of extraLinks) {
if (type == "separator") {
filteredLinks.push({ type });
continue;
}
if (url && seenLinks[url])
continue;
seenLinks[url] = true;
if (url) {
try {
url = new URL(url);
} catch (e) {
console.log("Couldn't parse profile URL:", url);
continue;
}
}
type ??= this._findLinkImageType(url);
type ??= "default-icon";
if (seenTypes[type] && type != "default-icon" && type != "shopping-cart" && type != "webpage-link")
continue;
seenTypes[type] = true;
let icon = linkTypes[type];
if (type == "twitter") {
let parts = url.pathname.split("/");
label = parts.length > 1 ? "@" + parts[1] : "Twitter";
}
filteredLinks.push({ url, type, icon, label, ...other });
}
if (filteredLinks.length && filteredLinks[filteredLinks.length - 1].type == "separator")
filteredLinks.splice(filteredLinks.length - 1, 1);
return filteredLinks;
}
_findLinkImageType(url) {
url = new URL(url);
let altIcons = {
"shopping-cart": ["dlsite.com", "skeb.jp", "ko-fi.com", "dmm.co.jp"],
commercial: ["fantia.jp"],
twitter: ["twitter.com"],
fanbox: ["fanbox.cc"],
booth: ["booth.pm"],
twitch: ["twitch.tv"]
};
if ((url.hostname == "pixiv.net" || url.hostname == "www.pixiv.net") && url.pathname.startsWith("/fanbox/"))
return "fanbox";
for (let alt in altIcons) {
for (let domain of altIcons[alt]) {
if (url.hostname == domain)
return alt;
if (url.hostname.endsWith("." + domain))
return alt;
}
}
return null;
}
};
var FollowTagWidget = class extends Widget {
constructor({ userId, ...options }) {
super({
...options,
template: \`<div class=vertical-list></div>\`
});
this.userId = userId;
this.load();
ppixiv.userCache.addEventListener(
"usermodified",
({ userId: userId2 }) => {
if (userId2 == this.userId)
this.load();
},
this._signal
);
}
async load() {
let userInfo = await ppixiv.userCache.getUserInfo(this.userId);
let selectedTags = /* @__PURE__ */ new Set();
if (userInfo?.isFollowed) {
let followInfo = await ppixiv.userCache.getUserFollowInfo(this.userId);
selectedTags = followInfo.tags;
}
let allTags = await ppixiv.userCache.loadAllUserFollowTags();
let followTagList = this.root;
helpers.html.removeElements(followTagList);
let addTagButton = helpers.createBoxLink({
label: "Add new tag",
icon: "add_circle",
classes: ["follow-tag"],
asElement: true
});
addTagButton.addEventListener("click", (e) => this._addFollowTag());
followTagList.appendChild(addTagButton);
allTags.sort(
(lhs, rhs) => lhs.toLowerCase().localeCompare(rhs.toLowerCase())
);
for (let tag of allTags) {
let button = helpers.createBoxLink({
label: tag,
classes: ["follow-tag"],
icon: "bookmark",
asElement: true
});
let selected = selectedTags.has(tag);
helpers.html.setClass(button, "selected", selected);
followTagList.appendChild(button);
button.addEventListener("click", (e) => {
this._toggleFollowTag(tag);
});
}
}
async _addFollowTag() {
let prompt = new TextPrompt({ title: "New folder:" });
let folder = await prompt.result;
if (folder == null)
return;
await this._toggleFollowTag(folder);
}
async _toggleFollowTag(tag) {
let userId = this.userId;
let userData = await ppixiv.userCache.getUserInfo(userId);
if (!userData.isFollowed) {
await Actions.follow(userId, null, { tag });
return;
}
let followInfo = await ppixiv.userCache.getUserFollowInfo(userId);
if (followInfo == null) {
console.log("Error retrieving follow info to update tags");
return;
}
let tagWasSelected = followInfo.tags.has(tag);
Actions.changeFollowTags(userId, { tag, add: !tagWasSelected });
}
};
/\x2f web/vview/widgets/menu-option.js
var menu_option_exports = {};
__export(menu_option_exports, {
MenuOption: () => MenuOption,
MenuOptionButton: () => MenuOptionButton,
MenuOptionNestedButton: () => MenuOptionNestedButton,
MenuOptionOptionsSetting: () => MenuOptionOptionsSetting,
MenuOptionRow: () => MenuOptionRow,
MenuOptionSlider: () => MenuOptionSlider,
MenuOptionSliderSetting: () => MenuOptionSliderSetting,
MenuOptionToggle: () => MenuOptionToggle,
MenuOptionToggleSetting: () => MenuOptionToggleSetting,
MenuOptionsThumbnailSizeSlider: () => MenuOptionsThumbnailSizeSlider
});
/\x2f web/vview/widgets/simple.js
var CheckboxWidget = class extends Widget {
constructor({
value = false,
...options
}) {
super({ ...options, template: \`
\${helpers.createIcon("", { classes: ["checkbox"] })}
\` });
this._checked = true;
}
set checked(value) {
if (this._checked == value)
return;
this._checked = value;
this.refresh();
}
get checked() {
return this._checked;
}
async refresh() {
this.root.innerText = this.checked ? "check_box" : "check_box_outline_blank";
}
};
var SliderWidget = class extends Widget {
constructor({
value = 0,
min = 0,
max = 10,
onchange = ({ value: value2 }) => {
},
...options
}) {
super({
...options,
template: \`
<div class=slider>
<div class=track-left></div>
<div class=track-right></div>
<div class=thumb></div>
</div>
\`
});
this._value = value;
this._min = min;
this._max = max;
this._onchange = onchange;
this.dragger = new DragHandler({
parent: this,
name: "slider",
deferredStart: () => false,
element: this.parent.root,
ondragstart: (args) => this._ondrag(args),
ondrag: (args) => this._ondrag(args)
});
}
get value() {
return this._value;
}
get min() {
return this._min;
}
get max() {
return this._max;
}
set value(value) {
if (this._value == value)
return;
this._value = value;
this.refresh();
}
set min(value) {
if (this._min == value)
return;
this._min = value;
this.refresh();
}
set max(value) {
if (this._max == value)
return;
this._max = value;
this.refresh();
}
_ondrag({ event }) {
let { left, right } = this.root.getBoundingClientRect();
let newValue = helpers.math.scaleClamp(event.clientX, left, right, this._min, this._max);
newValue = Math.round(newValue);
if (this._value == newValue)
return true;
this._value = newValue;
this.refresh();
this._onchange({ value: this._value });
return true;
}
refresh() {
let percent = helpers.math.scaleClamp(this._value, this._min, this._max, 0, 100);
this.root.style.setProperty("--fill", \`\${percent}%\`);
}
};
/\x2f web/vview/widgets/menu-option.js
var MenuOption = class extends Widget {
constructor({
classes = [],
refresh = null,
shouldBeVisible = null,
...options
}) {
super(options);
this.explanationNode = this.querySelector(".explanation");
this.shouldBeVisible = shouldBeVisible;
if (shouldBeVisible != null)
ppixiv.settings.addEventListener("all", () => this.callVisibilityChanged(), this._signal);
for (let className of classes)
this.root.classList.add(className);
this.onrefresh = refresh;
}
applyVisibility() {
if (this.shouldBeVisible == null)
return super.applyVisibility();
helpers.html.setClass(this.root, "hidden-widget", !this.shouldBeVisible());
}
refresh() {
if (this.onrefresh)
this.onrefresh();
this.refreshExplanation();
}
get explanationText() {
return null;
}
refreshExplanation() {
if (this.explanationNode == null)
return;
let text = this.explanationText;
if (typeof text == "function")
text = text();
this.explanationNode.hidden = text == null;
this.explanationNode.innerText = text;
}
};
var MenuOptionRow = class extends MenuOption {
constructor({
label = null,
...options
}) {
super({ ...options, template: \`
<div class=box-link-row>
<span class=label-box style="flex: 1;" hidden></span>
</div>
\` });
this.label = label;
}
set label(label) {
let span = this.root.querySelector(".label-box");
span.hidden = label == null;
span.innerText = label ?? "";
}
};
var MenuOptionButton = class extends MenuOption {
constructor({
url = null,
label,
getLabel = null,
onclick = null,
explanationEnabled = null,
explanationDisabled = null,
popup = null,
icon = null,
...options
}) {
super({ ...options, template: \`
\${helpers.createBoxLink({
label,
icon,
link: url,
popup,
classes: ["menu-toggle"],
explanation: ""
})}
\` });
this._clickHandler = onclick;
this._enabled = true;
this.explanationEnabled = helpers.other.makeFunction(explanationEnabled);
this.explanationDisabled = helpers.other.makeFunction(explanationDisabled ?? explanationEnabled);
this.getLabel = getLabel;
if (this._clickHandler != null)
this.root.classList.add("clickable");
this.root.querySelector(".label").innerText = label;
this.root.addEventListener("click", this.onclick);
}
refresh() {
super.refresh();
if (this.getLabel)
this.root.querySelector(".label").innerText = this.getLabel();
}
set enabled(value) {
helpers.html.setClass(this.root, "disabled", !value);
this._enabled = value;
}
get enabled() {
return this._enabled;
}
get explanationText() {
return this.enabled ? this.explanationEnabled() : this.explanationDisabled();
}
onclick = (e) => {
if (!this._enabled) {
e.preventDefault();
return;
}
if (this._clickHandler) {
this._clickHandler(e);
}
};
};
var MenuOptionNestedButton = class extends MenuOption {
constructor({
onclick = null,
label,
...options
}) {
super({ ...options, template: helpers.createBoxLink({ label: "", classes: ["clickable"] }) });
this.root.querySelector(".label").innerText = label;
this.root.addEventListener("click", (e) => {
e.stopPropagation();
e.preventDefault();
onclick(e);
});
}
};
var MenuOptionToggle = class extends MenuOptionButton {
constructor({
checked = false,
...options
}) {
super({ ...options });
this.checkbox = new CheckboxWidget({ container: this.querySelector(".widget-box") });
this.checkbox.checked = checked;
}
get value() {
return false;
}
set value(value) {
}
get explanationText() {
return this.value ? this.explanationEnabled : this.explanationDisabled;
}
};
var MenuOptionToggleSetting = class extends MenuOptionToggle {
constructor({
setting = null,
onclick = null,
settings = null,
onValue = true,
offValue = false,
...options
}) {
super({
...options,
onclick: (e) => {
if (this.options && this.options.check && !this.options.check())
return;
this.value = !this.value;
if (onclick)
onclick(e);
}
});
this.settings = settings ?? ppixiv.settings;
this.setting = setting;
this.onValue = onValue;
this.offValue = offValue;
if (this.setting)
this.settings.addEventListener(this.setting, this.refresh.bind(this), { signal: this.shutdownSignal });
}
refresh() {
super.refresh();
let value = this.value;
if (this.options.invertDisplay)
value = !value;
this.checkbox.checked = value;
}
get value() {
return this.settings.get(this.setting) == this.onValue;
}
set value(value) {
this.settings.set(this.setting, value ? this.onValue : this.offValue);
this.refresh();
}
};
var MenuOptionSlider = class extends MenuOption {
constructor({
min = null,
max = null,
list = null,
...options
}) {
super({ ...options, template: \`
<vv-container class=menu-slider></vv-container>
\` });
this.slider = new SliderWidget({
container: this.root,
onchange: ({ value }) => {
this.value = this.sliderValue;
}
});
this.list = list;
if (this.list != null) {
min = 0;
max = this.list.length - 1;
}
this.slider.min = min;
this.slider.max = max;
}
refresh() {
this.sliderValue = this.value;
super.refresh();
}
get value() {
return parseInt(super.value);
}
set value(value) {
super.value = value;
}
_sliderIndexToValue(value) {
if (this.list == null)
return value;
return this.list[value];
}
_valueToSliderIndex(value) {
if (this.list == null)
return value;
let closestIndex = -1;
let closestDistance = null;
for (let idx = 0; idx < this.list.length; ++idx) {
let v = this.list[idx];
if (value == v)
return idx;
let distance2 = Math.abs(value - v);
if (closestDistance == null || distance2 < closestDistance) {
closestIndex = idx;
closestDistance = distance2;
}
}
return closestIndex;
}
set sliderValue(value) {
value = this._valueToSliderIndex(value);
if (this.slider.value == value)
return;
this.slider.value = value;
}
get sliderValue() {
let value = parseInt(this.slider.value);
value = this._sliderIndexToValue(value);
return value;
}
};
var MenuOptionSliderSetting = class extends MenuOptionSlider {
constructor({
setting,
settings = null,
...options
}) {
super(options);
this.setting = setting;
this.settings = settings ?? ppixiv.settings;
}
get minValue() {
return this.options.min;
}
get maxValue() {
return this.options.max;
}
get value() {
return this.settings.get(this.setting);
}
set value(value) {
this.settings.set(this.setting, value);
this.refresh();
}
};
var MenuOptionOptionsSetting = class extends MenuOptionButton {
constructor({
setting,
label,
values,
explanation,
settings = null,
...options
}) {
super({
...options,
label
});
this._getExplanation = explanation;
this.settings = settings ?? ppixiv.settings;
this.setting = setting;
this.button = helpers.createBoxLink({
label,
icon: "expand_more",
classes: ["menu-dropdown-button", "clickable"],
asElement: true
});
this.querySelector(".widget-box").appendChild(this.button);
this.opener = new DropdownMenuOpener({
button: this.button,
createDropdown: ({ ...options2 }) => {
let dropdown = new Widget({
...options2,
template: \`<div class=vertical-list></div>\`
});
let currentValue = this.value;
for (let [value, label2] of Object.entries(values)) {
let link = helpers.createBoxLink({ label: label2, asElement: true });
helpers.html.setClass(link, "selected", value == currentValue);
dropdown.root.appendChild(link);
link.addEventListener("click", () => {
this.value = value;
});
}
return dropdown;
}
});
}
get value() {
return this.settings.get(this.setting);
}
set value(value) {
this.settings.set(this.setting, value);
this.refresh();
}
refresh() {
super.refresh();
this.opener.setButtonPopupHighlight();
}
get explanationText() {
if (!this._getExplanation)
return null;
return this._getExplanation(this.value);
}
};
var MenuOptionsThumbnailSizeSlider = class extends MenuOptionSliderSetting {
constructor({ ...options }) {
super(options);
this.refresh();
}
move(down) {
ppixiv.settings.adjustZoom(this.setting, down);
}
get value() {
let value = super.value;
if (typeof value != "number" || isNaN(value))
value = 4;
return value;
}
set value(value) {
super.value = value;
}
static thumbnailSizeForValue(value) {
return 100 * Math.pow(1.3, value);
}
};
/\x2f web/vview/misc/image-translation.js
var MaxParallelTranslationRequests = 3;
var AllSettings = {
translation_low_res: "forceLowRes",
translation_size: "size",
translation_translator: "translator",
translation_direction: "direction",
translation_language: "target_language"
};
var TranslationError = class extends Error {
};
var ImageTranslations = class extends EventTarget {
constructor() {
super();
this._displayedMediaId = null;
this._translateMediaIds = /* @__PURE__ */ new Set();
this._translations = /* @__PURE__ */ new Map();
this._translationRequests = /* @__PURE__ */ new Map();
this._settingsToId = /* @__PURE__ */ new Map();
this._mediaIdSettingsOverrides = /* @__PURE__ */ new Map();
for (let settingsKey of Object.keys(AllSettings)) {
ppixiv.settings.addEventListener(settingsKey, () => {
this._checkTranslationQueue();
this._callTranslationUrlsListeners();
});
}
}
get supported() {
return !ppixiv.native && !ppixiv.ios;
}
_callTranslationUrlsListeners() {
this.dispatchEvent(new Event("translation-urls-changed"));
}
setDisplayedMediaId(mediaId) {
this._displayedMediaId = mediaId;
this._checkTranslationQueue();
}
setTranslationsEnabled(mediaId, enabled) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
if (enabled)
this._translateMediaIds.add(mediaId);
else
this._translateMediaIds.delete(mediaId);
this._checkTranslationQueue();
this._callTranslationUrlsListeners();
}
getTranslationsEnabled(mediaId) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
return this._translateMediaIds.has(mediaId);
}
_checkTranslationQueue() {
this._refreshTranslationIndicator();
if (this._translationRequests.size >= MaxParallelTranslationRequests)
return;
let mediaId = this._displayedMediaId;
let firstPageMediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
if (mediaId == null || !this._translateMediaIds.has(firstPageMediaId))
return;
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: true });
if (mediaInfo == null) {
ppixiv.mediaCache.getMediaInfo(mediaId, { full: true }).then(() => {
console.log("Check queue after fetching info");
this._checkTranslationQueue();
});
return;
}
let [_, currentPage] = helpers.mediaId.toIllustIdAndPage(mediaId);
let pagesToLoad = [];
for (let page = currentPage; page < mediaInfo.pageCount; ++page)
pagesToLoad.push(page);
for (let page = currentPage - 1; page >= 0; --page)
pagesToLoad.push(page);
for (let page of pagesToLoad) {
if (this._translationRequests.size >= MaxParallelTranslationRequests)
break;
let pageMediaId = helpers.mediaId.getMediaIdForPage(mediaId, page);
if (this._translations.has(this._getIdForMediaId(pageMediaId)) || this._translationRequests.has(this._getIdForMediaId(pageMediaId)))
continue;
let promise = this._getMediaIdTranslation(pageMediaId, page);
this._translationRequests.set(this._getIdForMediaId(pageMediaId), promise);
promise.finally(() => {
this._translationRequests.delete(this._getIdForMediaId(pageMediaId), promise);
});
promise.then(async () => {
await helpers.other.sleep(250);
this._checkTranslationQueue();
return false;
});
}
this._refreshTranslationIndicator();
}
_refreshTranslationIndicator() {
let showLoadingIndicator = this._displayedMediaId != null && this.getTranslationsEnabled(this._displayedMediaId) && !this._translations.has(this._getIdForMediaId(this._displayedMediaId));
helpers.html.setDataSet(document.documentElement.dataset, "loadingTranslation", showLoadingIndicator);
}
async _getMediaIdTranslation(mediaId, page) {
console.log(\`Requesting translation for \${mediaId}\`);
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: true });
console.assert(mediaInfo != null);
let translationUrl;
try {
translationUrl = await this._translateImage(mediaInfo, page);
} catch (e) {
let log = \`Error translating \${mediaInfo.mediaId}: \${e.message}\`;
if (e instanceof TranslationError)
console.log(log);
else
console.error(log);
translationUrl = e;
}
let blankImage2 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQI12NgYAAAAAMAASDVlMcAAAAASUVORK5CYII=";
if (translationUrl == blankImage2)
translationUrl = null;
if (translationUrl != null && !(translationUrl instanceof Error))
helpers.other.preloadImages([translationUrl]);
this._translations.set(this._getIdForMediaId(mediaId), translationUrl);
this._callTranslationUrlsListeners();
}
getTranslationUrl(mediaId) {
let firstPageMediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
if (!this._translateMediaIds.has(firstPageMediaId))
return null;
let url = this._translations.get(this._getIdForMediaId(mediaId));
if (url instanceof Error)
return null;
else
return url;
}
getTranslationError(mediaId) {
let firstPageMediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
if (!this._translateMediaIds.has(firstPageMediaId))
return null;
let url = this._translations.get(this._getIdForMediaId(mediaId));
if (url instanceof Error)
return url.message;
else
return null;
}
retryTranslation(mediaId) {
let id = this._getIdForMediaId(mediaId);
let url = this._translations.get(id);
if (url instanceof Error)
this._translations.delete(id);
this._checkTranslationQueue();
this._callTranslationUrlsListeners();
}
_settingsForImage(mediaId) {
let settings = {
detector: "default"
};
let overrides = this._mediaIdSettingsOverrides.get(mediaId) ?? {};
for (let [settingsKey, apiKey] of Object.entries(AllSettings))
settings[apiKey] = overrides[settingsKey] ?? ppixiv.settings.get(settingsKey);
return settings;
}
getSettingHandlerForImage(mediaId) {
return {
get: (settingName) => {
return this.getSettingForImage(mediaId, settingName);
},
set: (settingName, value) => {
this.setSettingForImage(mediaId, settingName, value);
this._callTranslationUrlsListeners();
},
addEventListener: () => null
};
}
getSettingForImage(mediaId, settingName) {
let defaultValue = ppixiv.settings.get(settingName);
let overrides = this._mediaIdSettingsOverrides.get(mediaId);
if (overrides == null)
return defaultValue;
return overrides[settingName] ?? defaultValue;
}
setSettingForImage(mediaId, settingName, value) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
let overrides = this._mediaIdSettingsOverrides.get(mediaId);
if (overrides == null) {
overrides = {};
this._mediaIdSettingsOverrides.set(mediaId, overrides);
}
let defaultValue = ppixiv.settings.get(settingName);
if (value == defaultValue)
delete overrides[settingName];
else
overrides[settingName] = value;
this._checkTranslationQueue();
}
_idForSettings(settings) {
settings = JSON.stringify(settings);
let settingsId = this._settingsToId.get(settings);
if (settingsId != null)
return settingsId;
settingsId = this._settingsToId.size;
this._settingsToId.set(settings, settingsId);
return settingsId;
}
_getIdForMediaId(mediaId) {
let settings = this._settingsForImage(mediaId);
let settingsId = this._idForSettings(settings);
return \`\${mediaId}|\${settingsId}\`;
}
async _translateImage(mediaInfo, page) {
let pageMediaId = helpers.mediaId.getMediaIdForPage(mediaInfo.mediaId, page);
let settings = this._settingsForImage(mediaInfo.mediaId);
let { size, translator, direction, detector, target_language, forceLowRes } = settings;
let { url } = mediaInfo.getMainImageUrl(page, { forceLowRes });
url = helpers.pixiv.adjustImageUrlHostname(url);
console.log(\`Downloading image for translation: \${url}\`);
let file = await downloadPixivImage(url);
console.log(\`Got image: \${url}\`);
if (!forceLowRes)
file = await this._preprocessImage(file);
let translationApiUrl = ppixiv.settings.get("translation_api_url") + "/task/upload/v1";
let response;
try {
console.log(\`Sending image for translation: \${url}\`);
response = await sendRequest2({
url: translationApiUrl,
method: "POST",
responseType: "text",
formData: {
size,
translator,
direction,
detector,
target_language,
retry: "false",
file
}
});
} catch (e) {
throw new TranslationError(e);
}
response = JSON.parse(response);
let { id, error, translation_mask } = response;
if (error != null)
throw new TranslationError(\`Translation error for \${pageMediaId}: \${error}\`);
if (translation_mask != null) {
console.log(\`Cached translation result for \${pageMediaId}: \${translation_mask}\`);
return translation_mask;
}
if (id == null) {
throw new TranslationError(\`Unexpected translation response for \${pageMediaId}:\`, response);
}
let websocket = new WebSocket(\`wss:/\x2fapi.cotrans.touhou.ai/task/\${id}/event/v1\`);
if (!await helpers.other.waitForWebSocketOpened(websocket))
throw new TranslationError("Couldn't connect to translation socket");
try {
while (1) {
let data = await helpers.other.waitForWebSocketMessage(websocket);
if (data == null)
throw new TranslationError(\`Translation socket closed without a result: \${pageMediaId}\`);
;
switch (data.type) {
case "status":
continue;
case "pending":
continue;
case "result":
console.log(\`Translation result for \${pageMediaId}: \${data.result.translation_mask}\`);
return data.result.translation_mask;
case "error":
throw new TranslationError(\`Translation error for \${pageMediaId}: \${data.error}\`);
case "not_found":
throw new TranslationError(\`Translation error for \${pageMediaId}: ID not found\`);
default:
console.log(\`Unknown translation queue message for \${pageMediaId}}:\`, data);
continue;
}
}
} finally {
websocket.close();
}
}
async _preprocessImage(data) {
let u8 = new Uint8Array(data);
let isJpeg = u8[0] == 255 && u8[1] == 216 && u8[2] == 255 && u8[3] == 224;
let blob = new Blob([data]);
let blobUrl = URL.createObjectURL(blob);
try {
let img = document.createElement("img");
img.src = blobUrl;
let result = await helpers.other.waitForImageLoad(img);
if (result == "failed") {
console.log(\`Image load failed\`);
return null;
}
let width = img.naturalWidth;
let height = img.naturalHeight;
let maxSize = 2048;
let resizeBy = 1;
resizeBy = Math.min(resizeBy, maxSize / width);
resizeBy = Math.min(resizeBy, maxSize / height);
if (resizeBy == 1 && isJpeg)
return data;
let canvas = document.createElement("canvas");
canvas.width = Math.round(width * resizeBy);
canvas.height = Math.round(height * resizeBy);
let context = canvas.getContext("2d");
context.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height);
return await helpers.other.canvasToBlob(canvas, { type: "image/jpeg", quality: 0.75 });
} finally {
URL.revokeObjectURL(blobUrl);
}
}
async getTranslatedImage(mediaId) {
if (!this.getTranslationsEnabled(mediaId))
return null;
let [_, page] = helpers.mediaId.toIllustIdAndPage(mediaId);
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, { full: true });
let { url } = mediaInfo.getMainImageUrl(page);
await this.waitForTranslation(mediaId);
let translationUrl = this.getTranslationUrl(mediaId);
if (translationUrl == null)
return null;
let canvas = document.createElement("canvas");
let context = canvas.getContext("2d");
let createdCanvas = false;
for (let imageUrl of [
url,
translationUrl
]) {
let arrayBuffer = await downloadPixivImage(imageUrl);
let blob = new Blob([arrayBuffer]);
let imageBlobUrl = URL.createObjectURL(blob);
let img = document.createElement("img");
img.src = imageBlobUrl;
try {
let imageLoadResult = await helpers.other.waitForImageLoad(img);
if (imageLoadResult == "failed") {
console.log(\`Image load failed: \${imageUrl}\`);
return null;
}
} finally {
URL.revokeObjectURL(imageBlobUrl);
}
if (!createdCanvas) {
createdCanvas = true;
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
}
context.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height);
}
return canvas;
}
waitForTranslation(mediaId, { signal } = {}) {
return new Promise((resolve) => {
let isReady = () => {
if (!this.getTranslationsEnabled(mediaId)) {
console.error(\`Translations not enabled for \${mediaId}\`);
return true;
}
return this._translations.has(this._getIdForMediaId(mediaId));
};
if (isReady()) {
resolve();
return;
}
let cleanupAbort = new AbortController();
ppixiv.mediaCache.addEventListener("mediamodified", (e) => {
if (isReady()) {
cleanupAbort.abort();
resolve();
}
}, { signal: cleanupAbort.signal });
});
}
};
var MenuOptionToggleImageTranslation = class extends MenuOptionToggle {
constructor({ mediaId, ...options }) {
super({
label: "Translate this image",
onclick: (e) => this.value = !this.value,
...options
});
this.mediaId = mediaId;
}
refresh() {
super.refresh();
this.checkbox.checked = this.value;
}
get value() {
return ppixiv.imageTranslations.getTranslationsEnabled(this.mediaId);
}
set value(value) {
ppixiv.imageTranslations.setTranslationsEnabled(this.mediaId, value);
this.refresh();
}
};
var MenuOptionRetryTranslation = class extends MenuOptionRow {
constructor({ mediaId, ...options }) {
super({
label: "There was an error translating this image",
onclick: (e) => this.value = !this.value,
...options
});
new MenuOptionButton({
icon: "wallpaper",
label: "Retry",
container: this.root,
onclick: () => {
ppixiv.imageTranslations.retryTranslation(this.mediaId);
}
});
ppixiv.imageTranslations.addEventListener("translation-urls-changed", () => this.refresh(), this._signal);
this.mediaId = mediaId;
}
refresh() {
super.refresh();
let error = ppixiv.imageTranslations.getTranslationError(this.mediaId);
this.visible = error;
}
};
function createTranslationSettingsWidget({ globalOptions, editOverrides }) {
let settings = ppixiv.settings;
let displayedMediaId = ppixiv.app.displayedMediaId;
if (editOverrides)
settings = editOverrides ? ppixiv.imageTranslations.getSettingHandlerForImage(displayedMediaId) : ppixiv.settings;
return {
translateThisImage: () => {
return new MenuOptionToggleImageTranslation({
...globalOptions,
mediaId: displayedMediaId
});
},
translationLanguage: () => {
return new MenuOptionOptionsSetting({
...globalOptions,
setting: "translation_language",
settings,
label: "Language",
values: {
ENG: "English",
CHS: "Chinese (Simplified)",
CHT: "Chinese (Traditional)",
CSY: "Czech",
NLD: "Dutch",
FRA: "French",
DEU: "German",
HUN: "Hungarian",
ITA: "Italian",
JPN: "Japanese",
KOR: "Korean",
PLK: "Polish",
PTB: "Portuguese (Brazil)",
ROM: "Romanian",
RUS: "Russian",
ESP: "Spanish",
TRK: "Turkish",
UKR: "Ukrainian",
VIN: "Vietnames",
ARA: "Arabic",
SRP: "Serbian",
HRV: "Croatian"
}
});
},
translationTranslator: () => {
return new MenuOptionOptionsSetting({
...globalOptions,
setting: "translation_translator",
settings,
label: "Translation engine",
values: {
"gpt3.5": "GPT3.5",
googleL: "Google",
youdao: "Youdao",
baidu: "Baidu",
deepl: "DeepL",
papago: "Papago",
offline: "Offline",
none: "None (remove text)"
}
});
},
translationLowRes: () => {
return new MenuOptionToggleSetting({
...globalOptions,
setting: "translation_low_res",
settings,
label: "Use low res image for translations (faster)"
});
},
translationSize: () => {
return new MenuOptionOptionsSetting({
...globalOptions,
setting: "translation_size",
settings,
label: "Translation resolution",
values: {
S: "1024x1024",
M: "1536x1536",
L: "2048x2048",
XL: "2560x2560"
}
});
},
translationDirection: () => {
return new MenuOptionOptionsSetting({
...globalOptions,
setting: "translation_direction",
settings,
label: "Text direction",
values: {
auto: "Automatic",
h: "Horizontal",
v: "Vertical"
}
});
},
retryTranslation: () => {
return new MenuOptionRetryTranslation({
...globalOptions,
mediaId: displayedMediaId
});
}
};
}
function createTranslationSettingsWidgets({ globalOptions, editOverrides }) {
let settingsWidgets = createTranslationSettingsWidget({ globalOptions, editOverrides });
if (editOverrides) {
new Widget({
...globalOptions,
template: \`
<div style="padding: 0.5em;">
These settings will only affect this image, and aren't saved. Settings for
all images can be changed from settings.
</div>
\`
});
settingsWidgets.translateThisImage();
}
settingsWidgets.translationLanguage();
settingsWidgets.translationTranslator();
settingsWidgets.translationSize();
settingsWidgets.translationDirection();
settingsWidgets.retryTranslation();
}
/\x2f web/vview/widgets/mutes.js
var EditMutedTagsWidget = class extends Widget {
constructor({
muteType,
...options
}) {
super({ ...options, template: \`
<div class=muted-tags-popup>
<span class=add-muted-user-box>
Users can be muted from their user page, or by right-clicking an image and clicking
\${helpers.createIcon("settings")}.
</span>
<span class=non-premium-mute-warning>
\${helpers.createBoxLink({ label: "Note", icon: "warning", classes: ["mute-warning-button"] })}
</span>
<div class=mute-warning>
<div>
You can mute any number of tags and users.
</div>
<p>
<div>
However, since you don't have Premium, mutes will only be saved in your browser
and can't be saved to your Pixiv account. They will be lost if you change
browsers or clear site data.
</div>
</div>
<div class=add-muted-tag-box> <!-- prevent full-line button styling -->
\${helpers.createBoxLink({ label: "Add", icon: "add", classes: ["add-muted-tag"] })}
</div>
<div class=mute-list></div>
</div>
\` });
this._muteType = muteType;
this.root.querySelector(".add-muted-tag-box").hidden = muteType != "tag";
this.root.querySelector(".add-muted-user-box").hidden = muteType != "user";
this.root.querySelector(".add-muted-tag").addEventListener("click", this._clickedAddMutedTag);
this.root.querySelector(".mute-warning-button").addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
let muteWarning = this.root.querySelector(".mute-warning");
muteWarning.hidden = !muteWarning.hidden;
});
this.root.querySelector(".non-premium-mute-warning").hidden = ppixiv.pixivInfo.premium;
}
visibilityChanged() {
super.visibilityChanged();
if (this.visible) {
this.root.querySelector(".mute-warning").hidden = true;
this.refresh();
}
if (!this.visible)
this._clearMutedUserIdCache();
}
refresh = async () => {
if (!this.visible)
return;
if (this._muteType == "tag")
await this._refreshForTags();
else
await this._refrehsForUsers();
};
createEntry() {
return this.createTemplate({ name: "muted-tag-entry", html: \`
<div class=muted-tag>
<a href=# class="remove-mute clickable">
\${helpers.createIcon("delete")}
</a>
<span class=tag-name></span>
</div>
\` });
}
_refreshForTags = async () => {
let tagsToTranslate = [...ppixiv.muting.pixivMutedTags];
for (let mute of ppixiv.muting.extraMutes) {
if (mute.type == "tag")
tagsToTranslate.push(mute.value);
}
let translatedTags = await ppixiv.tagTranslations.getTranslations(tagsToTranslate);
let createMutedTagEntry = (tag, tagListContainer) => {
let entry = this.createEntry();
entry.dataset.tag = tag;
let label = tag;
let tagTranslation = translatedTags[tag];
if (tagTranslation)
label = \`\${tagTranslation} (\${tag})\`;
entry.querySelector(".tag-name").innerText = label;
tagListContainer.appendChild(entry);
return entry;
};
let mutedTagList = this.root.querySelector(".mute-list");
helpers.html.removeElements(mutedTagList);
for (let { type, value: tag } of ppixiv.muting.extraMutes) {
if (type != "tag")
continue;
let entry = createMutedTagEntry(tag, mutedTagList);
entry.querySelector(".remove-mute").addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
ppixiv.muting.removeExtraMute(tag, { type: "tag" });
this.refresh();
});
}
for (let tag of ppixiv.muting.pixivMutedTags) {
let entry = createMutedTagEntry(tag, mutedTagList);
entry.querySelector(".remove-mute").addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
await ppixiv.muting.removePixivMute(tag, { type: "tag" });
this.refresh();
});
}
};
_refrehsForUsers = async () => {
let createMutedTagEntry = (userId, username, tagListContainer) => {
let entry = this.createEntry();
entry.dataset.userId = userId;
entry.querySelector(".tag-name").innerText = username;
tagListContainer.appendChild(entry);
return entry;
};
let mutedUserList = this.root.querySelector(".mute-list");
helpers.html.removeElements(mutedUserList);
for (let { type, value: userId, label: username } of ppixiv.muting.extraMutes) {
if (type != "user")
continue;
let entry = createMutedTagEntry(userId, username, mutedUserList);
entry.querySelector(".remove-mute").addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
ppixiv.muting.removeExtraMute(userId, { type: "user" });
this.refresh();
});
}
let userIdToUsername = this._cachedMutedUserIdToUsername;
if (userIdToUsername == null) {
this._getMutedUserIdToUsername().then(() => {
console.log("Refreshing after muted user load");
this.refresh();
});
} else {
let mutes = ppixiv.muting.pixivMutedUserIds;
mutes.sort((lhs, rhs) => {
lhs = userIdToUsername[lhs] || "";
rhs = userIdToUsername[rhs] || "";
return lhs.localeCompare(rhs);
});
for (let userId of mutes) {
let entry = createMutedTagEntry(userId, userIdToUsername[userId], mutedUserList);
entry.querySelector(".remove-mute").addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
await ppixiv.muting.removePixivMute(userId, { type: "user" });
this.refresh();
});
}
}
};
_clearMutedUserIdCache() {
this._cachedMutedUserIdToUsername = null;
}
_getMutedUserIdToUsername() {
if (this._cachedMutedUserIdToUsername)
return this._cachedMutedUserIdToUsername;
if (this._mutedUserIdToUsernamePromise)
return this._mutedUserIdToUsernamePromise;
let promise = this._getMutedUserIdToUsernameInner();
this._mutedUserIdToUsernamePromise = promise;
this._mutedUserIdToUsernamePromise.finally(() => {
if (this._mutedUserIdToUsernamePromise == promise)
this._mutedUserIdToUsernamePromise = null;
});
return this._mutedUserIdToUsernamePromise;
}
async _getMutedUserIdToUsernameInner() {
let result = await helpers.pixivRequest.get("/ajax/mute/items", { context: "setting" });
if (result.error) {
ppixiv.message.show(result.message);
this._cachedMutedUserIdToUsername = {};
return this._cachedMutedUserIdToUsername;
}
let userIdToUsername = {};
for (let item of result.body.mute_items) {
if (item.type == "user")
userIdToUsername[item.value] = item.label;
}
this._cachedMutedUserIdToUsername = userIdToUsername;
return this._cachedMutedUserIdToUsername;
}
_clickedAddMutedTag = async (e) => {
e.preventDefault();
e.stopPropagation();
let prompt = new TextPrompt({ title: "Tag to mute:" });
let tag = await prompt.result;
if (tag == null || tag == "")
return;
if (ppixiv.pixivInfo.premium)
await ppixiv.muting.addPixivMute(tag, { type: "tag" });
else
await ppixiv.muting.addExtraMute(tag, tag, { type: "tag" });
this.refresh();
};
};
var MutedTagsForPostDialog = class extends DialogWidget {
constructor({
mediaId,
userId,
...options
}) {
super({ ...options, classes: "muted-tags-popup", header: "Edit mutes", template: \`
<div style="display: flex; align-items: center;">
<span class=non-premium-mute-warning>
\${helpers.createBoxLink({ label: "Note", icon: "warning", classes: ["mute-warning-button", "clickable"] })}
</span>
</div>
<div class=mute-warning hidden>
<div>
You can mute any number of tags and users.
</div>
<p>
<div>
However, since you don't have Premium, mutes will only be saved in your browser
and can't be saved to your Pixiv account. They will be lost if you change
browsers or clear site data.
</div>
</div>
<div class=post-mute-list></div>
\` });
this._mediaId = mediaId;
this._userId = userId;
this.root.querySelector(".close-button").addEventListener("click", (e) => {
this.shutdown();
}, { signal: this.shutdownSignal });
this.root.querySelector(".mute-warning-button").addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
let muteWarning = this.root.querySelector(".mute-warning");
muteWarning.hidden = !muteWarning.hidden;
});
this.root.querySelector(".non-premium-mute-warning").hidden = ppixiv.pixivInfo.premium;
this.refresh();
}
refresh = async () => {
if (this._mediaId != null) {
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(this._mediaId, { full: false });
await this._refreshForData(mediaInfo.tagList, mediaInfo.userId, mediaInfo.userName);
} else {
let userInfo = await ppixiv.userCache.getUserInfo(this._userId);
await this._refreshForData([], this._userId, userInfo.name);
}
};
async _refreshForData(tags, userId, username) {
let translatedTags = await ppixiv.tagTranslations.getTranslations(tags);
let createEntry = (label, isMuted) => {
let entry = this.createTemplate({ name: "muted-tag-or-user-entry", html: \`
<div class=entry>
\${helpers.createBoxLink({ label: "Mute", classes: ["toggle-mute"] })}
<span class=tag-name></span>
</div>
\` });
helpers.html.setClass(entry, "muted", isMuted);
entry.querySelector(".toggle-mute .label").innerText = isMuted ? "Muted" : "Mute";
entry.querySelector(".tag-name").innerText = label;
mutedList.appendChild(entry);
return entry;
};
let mutedList = this.root.querySelector(".post-mute-list");
helpers.html.removeElements(mutedList);
{
let isMuted = ppixiv.muting.isUserIdMuted(userId);
let entry = createEntry(\`User: \${username}\`, isMuted);
entry.querySelector(".toggle-mute").addEventListener("click", async (e) => {
if (isMuted) {
ppixiv.muting.removeExtraMute(userId, { type: "user" });
await ppixiv.muting.removePixivMute(userId, { type: "user" });
} else {
await ppixiv.muting.addMute(userId, username, { type: "user" });
}
this.refresh();
});
}
for (let tag of tags) {
let isMuted = ppixiv.muting.anyTagMuted([tag]);
let label = tag;
let tagTranslation = translatedTags[tag];
if (tagTranslation)
label = \`\${tagTranslation} (\${tag})\`;
let entry = createEntry(label, isMuted);
entry.querySelector(".toggle-mute").addEventListener("click", async (e) => {
if (isMuted) {
ppixiv.muting.removeExtraMute(tag, { type: "tag" });
await ppixiv.muting.removePixivMute(tag, { type: "tag" });
} else {
await ppixiv.muting.addMute(tag, tag, { type: "tag" });
}
this.refresh();
});
}
}
};
/\x2f web/vview/misc/media-info.js
var MediaInfoEvents = new EventTarget();
var pendingMediaIdCallbacks = /* @__PURE__ */ new Set();
var MediaInfo = class {
static callMediaInfoModifiedCallbacks(mediaId) {
let wasEmpty = pendingMediaIdCallbacks.size == 0;
pendingMediaIdCallbacks.add(mediaId);
if (wasEmpty)
realSetTimeout(() => this.flushMediaInfoModifiedCallbacks(), 0);
}
static flushMediaInfoModifiedCallbacks() {
let mediaIds = pendingMediaIdCallbacks;
pendingMediaIdCallbacks = /* @__PURE__ */ new Set();
for (let mediaId of mediaIds) {
let event = new Event("mediamodified");
event.mediaId = mediaId;
MediaInfoEvents.dispatchEvent(event);
}
}
get full() {
return this._getInfo("full");
}
get mediaId() {
return this._getInfo("mediaId");
}
get mediaType() {
return helpers.mediaId.parse(this.mediaId).type;
}
get illustId() {
return this._getInfo("illustId");
}
get illustType() {
return this._getInfo("illustType");
}
get illustTitle() {
return this._getInfo("illustTitle");
}
get pageCount() {
return this._getInfo("pageCount");
}
get userId() {
return this._getInfo("userId", null);
}
get userName() {
return this._getInfo("userName");
}
get width() {
return this._getInfo("width");
}
get height() {
return this._getInfo("height");
}
get previewUrls() {
return this._getInfo("previewUrls");
}
get bookmarkData() {
return this._getInfo("bookmarkData");
}
set bookmarkData(value) {
this._setInfo("bookmarkData", value);
}
get createDate() {
return this._getInfo("createDate");
}
get tagList() {
return this._getInfo("tagList");
}
get aiType() {
return this._getInfo("aiType", 0);
}
get likeCount() {
return this._getInfo("likeCount");
}
set likeCount(value) {
this._setInfo("likeCount", value);
}
get bookmarkCount() {
return this._getInfo("bookmarkCount");
}
set bookmarkCount(value) {
this._setInfo("bookmarkCount", value);
}
get extraData() {
return this._getInfo("extraData");
}
set extraData(value) {
this._setInfo("extraData", value);
}
get mangaPages() {
return this._getInfo("mangaPages");
}
get illustComment() {
return this._getInfo("illustComment", "");
}
get ugoiraMetadata() {
return this._getInfo("ugoiraMetadata");
}
get seriesNavData() {
return this._getInfo("seriesNavData");
}
get localPath() {
return this._getInfo("localPath");
}
_getInfo(name, defaultValue = void 0) {
if (!(name in this._info)) {
if (defaultValue !== void 0)
return defaultValue;
throw new Error(\`Field \${name} not available in image info for \${this._info.mediaId}\`);
}
return this._info[name];
}
_setInfo(name, value) {
if (!(name in this._info))
throw new Error(\`Field \${name} not available in image info for \${this._info.mediaId}\`);
if (this._info[name] === value)
return;
this._info[name] = value;
MediaInfo.callMediaInfoModifiedCallbacks(this.mediaId);
}
get partialInfo() {
return this;
}
get isLocal() {
return false;
}
static createFrom({ mediaInfo }) {
let classType;
if (mediaInfo.classType) {
let classes = {
VviewMediaInfo,
PixivMediaInfo
};
classType = classes[mediaInfo.classType];
mediaInfo = mediaInfo.info;
} else {
if (helpers.mediaId.isLocal(mediaInfo.mediaId))
classType = VviewMediaInfo;
else
classType = PixivMediaInfo;
mediaInfo = classType.preprocessInfo({ mediaInfo });
}
return new classType({ mediaInfo });
}
get serialize() {
let classType;
if (this.__proto__.constructor === VviewMediaInfo)
classType = "VviewMediaInfo";
else
classType = "PixivMediaInfo";
return {
classType,
info: this._info
};
}
static preprocessInfo({ mediaInfo }) {
return mediaInfo;
}
constructor({ mediaInfo }) {
this._info = { ...mediaInfo };
}
updateInfo(mediaInfo) {
console.assert(mediaInfo instanceof MediaInfo, mediaInfo);
for (let [key, value] of Object.entries(mediaInfo._info)) {
if (key == "full" && !value)
continue;
if (key == "mediaId") {
console.assert(value == this._info.mediaId, \`Can't change media ID from \${this._info.mediaId} to \${value}\`);
continue;
}
this._info[key] = value;
}
}
getMainImageUrl(page = 0, { ignoreLimits = false } = {}) {
let mangaPage = this.mangaPages[page];
if (mangaPage == null)
return {};
return {
url: mangaPage.urls.original,
width: mangaPage.width,
height: mangaPage.height
};
}
};
var partialPixivKeys = /* @__PURE__ */ new Set([
"full",
"mediaId",
"bookmarkData",
"createDate",
"tagList",
"extraData",
"illustTitle",
"illustType",
"userName",
"previewUrls",
"illustId",
"aiType",
"userId",
"pageCount",
"width",
"height"
]);
function createPartialPixivMediaInfo(mediaInfo) {
console.assert(mediaInfo instanceof PixivMediaInfo);
return new Proxy(mediaInfo, {
get(target, key, receiver) {
if (key == "then")
return void 0;
if (key == "full")
return false;
if (key.constructor === Symbol || target[key] instanceof Function)
return target[key];
if (!partialPixivKeys.has(key))
throw new Error(\`MediaInfo key \${key} isn't available in partial media info\`);
return target[key];
},
has(target, key) {
if (!partialPixivKeys.has(key))
throw new Error(\`MediaInfo key \${key} isn't available in partial media info\`);
return key in target;
},
set(obj, key, value) {
if (!partialPixivKeys.has(key))
throw new Error(\`MediaInfo key \${key} can't be set in partial media info\`);
obj[key] = value;
return true;
}
});
}
var PixivMediaInfo = class extends MediaInfo {
get partialInfo() {
return createPartialPixivMediaInfo(this);
}
getMainImageUrl(page = 0, { ignoreLimits = false, forceLowRes = false } = {}) {
let mangaPage = this.mangaPages[page];
if (mangaPage == null)
return {};
let pixels = mangaPage.width * mangaPage.height;
let maxPixels = ppixiv.settings.get("image_size_limit");
if (!ignoreLimits && maxPixels != null && pixels > maxPixels)
forceLowRes = true;
if (forceLowRes) {
let ratio = Math.min(1, 1200 / mangaPage.width, 1200 / mangaPage.height);
let width = Math.round(mangaPage.width * ratio);
let height = Math.round(mangaPage.height * ratio);
return { url: mangaPage.urls.regular, width, height };
}
return super.getMainImageUrl(page);
}
};
var VviewMediaInfo = class extends MediaInfo {
get full() {
return true;
}
get width() {
return this.mangaPages[0]?.width;
}
get height() {
return this.mangaPages[0]?.height;
}
get pageCount() {
return this.mediaType == "folder" ? 0 : 1;
}
get isLocal() {
return true;
}
get ugoiraMetadata() {
return null;
}
get seriesNavData() {
return null;
}
static preprocessInfo({ mediaInfo }) {
mediaInfo = { ...mediaInfo };
let { type } = helpers.mediaId.parse(mediaInfo.mediaId);
if (type == "folder") {
mediaInfo.mangaPages = [];
mediaInfo.userName = null;
mediaInfo.illustType = 0;
} else {
mediaInfo.mangaPages = [{
width: mediaInfo.width,
height: mediaInfo.height,
urls: mediaInfo.urls
}];
}
return mediaInfo;
}
getMainImageUrl(page = 0) {
let result = this._getMainImageUrlWithUpscaling(page);
return result ?? super.getMainImageUrl(page);
}
_getMainImageUrlWithUpscaling(page) {
if (this.illustType != 0)
return null;
let mangaPage = this.mangaPages[page];
if (mangaPage == null)
return null;
let upscaleSetting = ppixiv.settings.get("upscaling");
if (!upscaleSetting)
return null;
let { width, height } = mangaPage;
let maxSizeForUpscaling = 2e3;
if (width >= maxSizeForUpscaling && height > maxSizeForUpscaling)
return null;
return {
url: mangaPage.urls.upscale2x,
width: mangaPage.width,
height: mangaPage.height
};
}
};
/\x2f web/vview/misc/send-image.js
var SendImage = class {
constructor() {
this._sendImageChannel = new LocalBroadcastChannel("ppixiv:send-image");
this.tabId = this._createTabId();
this._tabIdTiebreaker = Date.now();
this._pendingMovement = [0, 0];
window.addEventListener("unload", this._windowUnload);
window.addEventListener(ppixiv.mobile ? "pointerdown" : "focus", (e) => {
this._finalizeQuickViewImage();
}, { capture: true });
ppixiv.mediaCache.addEventListener("mediamodified", ({ mediaId }) => {
this._broadcastMediaChanges(mediaId);
});
this._sendImageChannel.addEventListener("message", this.receivedMessage);
this._broadcastTabInfo();
this.sendMessage({ message: "list-tabs" });
}
get enabled() {
return ppixiv.native || !ppixiv.mobile;
}
_createTabId(recreate = false) {
let storage = ppixiv.android ? localStorage : sessionStorage;
if (!recreate && storage.ppixivTabId)
return storage.ppixivTabId;
storage.ppixivTabId = helpers.other.createUuid();
return storage.ppixivTabId;
}
_finalizeQuickViewImage = () => {
let args = helpers.args.location;
if (args.hash.has("temp-view")) {
console.log("Finalizing quick view image because we gained focus");
args.hash.delete("virtual");
args.hash.delete("temp-view");
helpers.navigate(args, { addToHistory: false });
}
};
messages = new EventTarget();
_windowUnload = (e) => {
this.sendMessage({
message: "send-image",
action: "cancel",
to: ppixiv.settings.get("linked_tabs", [])
});
};
async send_image(mediaId, tabIds, action) {
if (tabIds.length == 0)
return;
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId);
let userId = mediaInfo?.userId;
let userInfo = userId ? ppixiv.userCache.getUserInfoSync(userId) : null;
this.sendMessage({
message: "send-image",
from: this.tabId,
to: tabIds,
mediaId,
action,
mediaInfo: mediaInfo?.serialize,
userInfo,
origin: window.origin
}, false);
}
_broadcastMediaChanges(mediaId) {
if (this._handlingBroadcastedMediaInfo)
return;
this._broadcastImageInfo(mediaId);
}
_broadcastImageInfo(mediaId) {
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId);
let userId = mediaInfo?.userId;
let userInfo = userId ? ppixiv.userCache.getUserInfoSync(userId) : null;
this.sendMessage({
message: "image-info",
from: this.tabId,
mediaId,
mediaInfo: mediaInfo?.serialize,
bookmarkTags: ppixiv.extraCache.getBookmarkDetailsSync(mediaId),
userInfo,
origin: window.origin
}, false);
}
receivedMessage = async (e) => {
let data = e.data;
if (data.to && data.to.indexOf(this.tabId) == -1)
return;
let event = new Event(data.message);
event.message = data;
this.messages.dispatchEvent(event);
if (data.message == "tab-info") {
if (data.from == this.tabId) {
if (this._tabIdTiebreaker >= data.tab_id_tiebreaker) {
console.log("Creating a new tab ID due to ID conflict");
this.tabId = this._createTabId(true);
} else
console.log("Tab ID conflict (other tab will create a new ID)");
this._broadcastTabInfo();
}
} else if (data.message == "list-tabs") {
this._broadcastTabInfo();
} else if (data.message == "send-image") {
if (data.origin == window.origin) {
console.log("Registering cached image info");
let { mediaInfo, userInfo } = data;
if (userInfo != null)
ppixiv.userCache.addUserData(userInfo);
if (mediaInfo != null)
ppixiv.mediaCache.addFullMediaInfo(mediaInfo);
}
if (data.action == "finalize") {
let args = helpers.args.location;
args.hash.delete("virtual");
args.hash.delete("temp-view");
helpers.navigate(args, { addToHistory: false });
return;
}
if (data.action == "cancel") {
this.hidePreviewImage();
return;
}
console.assert(data.action == "temp-view" || data.action == "display", data.actionj);
ppixiv.app.showMediaId(data.mediaId, {
tempView: data.action == "temp-view",
source: "temp-view",
addToHistory: !ppixiv.phistory.virtual
});
} else if (data.message == "image-info") {
if (data.origin != window.origin)
return;
MediaInfo.flushMediaInfoModifiedCallbacks();
this._handlingBroadcastedMediaInfo = true;
try {
let { mediaInfo, bookmarkTags, userInfo } = data;
if (mediaInfo != null)
ppixiv.mediaCache.addFullMediaInfo(mediaInfo);
if (bookmarkTags != null)
ppixiv.extraCache.updateCachedBookmarkTags(data.mediaId, bookmarkTags);
if (userInfo != null)
ppixiv.userCache.addUserData(userInfo);
MediaInfo.flushMediaInfoModifiedCallbacks();
} finally {
this._handlingBroadcastedMediaInfo = false;
}
} else if (data.message == "preview-mouse-movement") {
if (!ppixiv.phistory.virtual)
return;
let event2 = new PointerEvent("quickviewpointermove", {
movementX: data.x,
movementY: data.y
});
if (event2.movementX == null) {
event2.movementX = data.x;
event2.movementY = data.y;
}
window.dispatchEvent(event2);
}
};
_broadcastTabInfo = () => {
let ourTabInfo = {
message: "tab-info",
tab_id_tiebreaker: this._tabIdTiebreaker
};
this.sendMessage(ourTabInfo);
};
sendMessage(data, send_to_self) {
data.from = this.tabId;
this._sendImageChannel.postMessage(data);
if (send_to_self) {
data = JSON.parse(JSON.stringify(data));
data.self = true;
this._sendImageChannel.dispatchEvent(new MessageEvent("message", { data }));
}
}
hidePreviewImage() {
let wasInPreview = ppixiv.phistory.virtual;
if (!wasInPreview)
return;
ppixiv.phistory.back();
}
sendMouseMovementToLinkedTabs(x, y) {
if (!ppixiv.settings.get("linked_tabs_enabled"))
return;
let tabIds = ppixiv.settings.get("linked_tabs", []);
if (tabIds.length == 0)
return;
this._pendingMovement[0] += x;
this._pendingMovement[1] += y;
if (this.lastMovementMessageTime != null && Date.now() - this.lastMovementMessageTime < 10)
return;
this.lastMovementMessageTime = Date.now();
this.sendMessage({
message: "preview-mouse-movement",
x: this._pendingMovement[0],
y: this._pendingMovement[1],
to: tabIds
}, false);
this._pendingMovement = [0, 0];
}
};
var LinkTabsPopup = class extends Widget {
constructor({ ...options }) {
super({
...options,
classes: "link-tab-popup",
template: \`
<div class="link-tab-popup">
<div class=explanation>
<ppixiv-inline src="resources/multi-monitor.svg" class=tutorial-monitor></ppixiv-inline>
<div style="margin: 10px 0 15px 0; font-size: 125%;">
Open a
<img src="ppixiv:resources/activate-icon.png" style="width: 28px; vertical-align: bottom;">
tab on another monitor and click "Link this tab" to send images to it
</div>
</div>
</div>
\`
});
}
sendLinkTabMessage = () => {
if (!this.visible)
return;
ppixiv.sendImage.sendMessage({
message: "show-link-tab",
linkedTabs: ppixiv.settings.get("linked_tabs", [])
});
};
visibilityChanged() {
super.visibilityChanged();
if (!this.visible) {
ppixiv.sendImage.sendMessage({ message: "hide-link-tab" });
return;
}
helpers.other.interval(this.sendLinkTabMessage, 1e3, this.visibilityAbort.signal);
ppixiv.settings.addEventListener("linked_tabs", this.sendLinkTabMessage, { signal: this.visibilityAbort.signal });
ppixiv.sendImage.messages.addEventListener("link-this-tab", (e) => {
let message = e.message;
let tabIds = ppixiv.settings.get("linked_tabs", []);
if (tabIds.indexOf(message.from) == -1)
tabIds.push(message.from);
ppixiv.settings.set("linked_tabs", tabIds);
this.sendLinkTabMessage();
}, this._signal);
ppixiv.sendImage.messages.addEventListener("unlink-this-tab", (e) => {
let message = e.message;
let tabIds = ppixiv.settings.get("linked_tabs", []);
let idx = tabIds.indexOf(message.from);
if (idx != -1)
tabIds.splice(idx, 1);
ppixiv.settings.set("linked_tabs", tabIds);
this.sendLinkTabMessage();
}, this._signal);
}
};
var LinkThisTabPopup = class extends DialogWidget {
static setup() {
let hideTimer = new Timer(() => {
this.visible = false;
});
let dialog = null;
ppixiv.sendImage.messages.addEventListener("show-link-tab", ({ message }) => {
LinkThisTabPopup.other_tab_id = message.from;
hideTimer.set(2e3);
if (dialog != null)
return;
dialog = new LinkThisTabPopup({ message });
dialog.shutdownSignal.addEventListener("abort", () => {
hideTimer.clear();
dialog = null;
});
ppixiv.sendImage.messages.addEventListener("hide-link-tab", ({ message: message2 }) => {
if (dialog)
dialog.visible = false;
}, dialog._signal);
});
}
constructor({
message,
...options
} = {}) {
super({
...options,
dialogClass: "simple-button-dialog",
dialogType: "small",
allowClose: false,
template: \`
\${helpers.createBoxLink({ label: "Link this tab", classes: ["link-this-tab"] })}
\${helpers.createBoxLink({ label: "Unlink this tab", classes: ["unlink-this-tab"] })}
\`
});
this._linkThisTab = this.querySelector(".link-this-tab");
this._unlinkThisTab = this.querySelector(".unlink-this-tab");
this._linkThisTab.hidden = true;
this._unlinkThisTab.hidden = true;
ppixiv.sendImage.messages.addEventListener("show-link-tab", ({ message: message2 }) => this.showLinkTabMessage({ message: message2 }), this._signal);
this._linkThisTab.addEventListener("click", (e) => {
ppixiv.sendImage.sendMessage({ message: "link-this-tab", to: [LinkThisTabPopup.other_tab_id] });
ppixiv.settings.set("linked_tabs", []);
}, this._signal);
this._unlinkThisTab.addEventListener("click", (e) => {
ppixiv.sendImage.sendMessage({ message: "unlink-this-tab", to: [LinkThisTabPopup.other_tab_id] });
}, this._signal);
this.showLinkTabMessage({ message });
}
showLinkTabMessage({ message }) {
let linked = message.linkedTabs.indexOf(ppixiv.sendImage.tabId) != -1;
this._linkThisTab.hidden = linked;
this._unlinkThisTab.hidden = !linked;
}
};
var SendImagePopup = class extends DialogWidget {
constructor({ mediaId, ...options } = {}) {
super({
...options,
showCloseButton: false,
dialogType: "small",
template: \`
<div>
Click a
<img src="ppixiv:resources/activate-icon.png" style="width: 28px; vertical-align: bottom;">
tab to send the image there
</div>
\`
});
this.root.addEventListener("click", (e) => {
if (e.target != this.root)
return;
this.visible = false;
});
helpers.other.interval(() => {
console.assert(this.visible);
ppixiv.sendImage.sendMessage({ message: "show-send-image" });
}, 1e3, this.shutdownSignal);
ppixiv.sendImage.messages.addEventListener("take-image", ({ message }) => {
let tabId = message.from;
ppixiv.sendImage.send_image(mediaId, [tabId], "display");
this.visible = false;
}, this._signal);
}
shutdown() {
super.shutdown();
ppixiv.sendImage.sendMessage({ message: "hide-send-image" });
}
};
var SendHerePopup = class extends DialogWidget {
static setup() {
let hideTimer = new Timer(() => {
this.visible = false;
});
let dialog = null;
ppixiv.sendImage.messages.addEventListener("show-send-image", ({ message }) => {
SendHerePopup.other_tab_id = message.from;
hideTimer.set(2e3);
if (dialog == null) {
dialog = new SendHerePopup();
dialog.shutdownSignal.addEventListener("abort", () => {
hideTimer.clear();
dialog = null;
});
}
}, this._signal);
ppixiv.sendImage.messages.addEventListener("hide-send-image", ({ message }) => {
if (dialog)
dialog.visible = false;
}, this._signal);
}
constructor({ ...options } = {}) {
super({
...options,
dialogClass: "simple-button-dialog",
small: true,
allowClose: false,
template: \`
\${helpers.createBoxLink({ label: "Click to send image here", classes: ["link-this-tab"] })}
\`
});
window.addEventListener("click", this.takeImage, { signal: this.shutdownSignal });
}
takeImage = (e) => {
ppixiv.sendImage.sendMessage({ message: "take-image", to: [SendHerePopup.other_tab_id] });
};
};
/\x2f web/vview/widgets/settings-widgets.js
function createSettingsWidget({ globalOptions }) {
return {
touchpadMode: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Enable touchpad mode",
setting: "touchpad_mode",
explanationEnabled: "Show context menu on pressed",
explanationDisabled: "Show context menu when holding"
});
},
thumbnailSize: () => {
const button = new MenuOptionButton({
...globalOptions,
label: "Thumbnail size"
});
new MenuOptionsThumbnailSizeSlider({
container: button.querySelector(".widget-box"),
setting: "thumbnail-size",
classes: ["size-slider"],
min: 0,
max: 7
});
},
disabledByDefault: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Disabled by default",
setting: "disabled-by-default",
explanationEnabled: "Go to Pixiv by default.",
explanationDisabled: "Go here by default."
});
},
noHideCursor: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Hide cursor",
setting: "no-hide-cursor",
invertDisplay: true,
explanationEnabled: "Hide the cursor while the mouse isn't moving.",
explanationDisabled: "Don't hide the cursor while the mouse isn't moving."
});
},
showMangaPageIndicator: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Show manga progress",
setting: "manga_page_indicator"
});
},
showAiIcon: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Show icon on AI-generated images",
setting: "show_ai_icon"
});
},
invertPopupHotkey: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Shift-right-click to show the popup menu",
setting: "invert-popup-hotkey",
explanationEnabled: "Shift-right-click to open the popup menu",
explanationDisabled: "Right click opens the popup menu"
});
},
ctrlOpensPopup: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Hold ctrl to show the popup menu",
setting: "ctrl_opens_popup",
explanationEnabled: "Pressing Ctrl shows the popup menu (for laptops)"
});
},
uiOnHover: () => {
new MenuOptionToggleSetting({
...globalOptions,
label: "Hover to show search box",
setting: "ui-on-hover",
explanationEnabled: "Only show the search box when hovering over it",
explanationDisabled: "Always show the search box"
});
},
invertScrolling: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Invert image panning",
setting: "invert-scrolling",
explanationEnabled: "Dragging down moves the image down",
explanationDisabled: "Dragging down moves the image up"
});
},
disableTranslations: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Show tag translations",
setting: "disable-translations",
invertDisplay: true
});
},
thumbnailStyle: () => {
return new MenuOptionOptionsSetting({
...globalOptions,
setting: "thumbnail_style",
label: "Thumbnail style",
values: {
aspect: "Aspect",
square: "Square"
}
});
},
disableThumbnailPanning: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Pan thumbnails while hovering over them",
setting: "disable_thumbnail_panning",
invertDisplay: true,
shouldBeVisible: () => ppixiv.settings.get("thumbnail_style") !== "aspect"
});
},
disableThumbnailZooming: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Zoom out thumbnails while hovering over them",
setting: "disable_thumbnail_zooming",
invertDisplay: true,
shouldBeVisible: () => ppixiv.settings.get("thumbnail_style") !== "aspect"
});
},
enableTransitions: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Use transitions",
setting: "animations_enabled"
});
},
bookmarkPrivatelyByDefault: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Bookmark and follow privately",
setting: "bookmark_privately_by_default",
explanationDisabled: ppixiv.mobile ? null : "Pressing Ctrl-B will bookmark publically",
explanationEnabled: ppixiv.mobile ? null : "Pressing Ctrl-B will bookmark privately"
});
},
limitSlideshowFramerate: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Limit slideshows to 60 FPS",
setting: "slideshow_framerate",
onValue: 60,
offValue: null
});
},
importExtraData: () => {
let widget = new MenuOptionRow({
...globalOptions,
label: "Image edits"
});
new MenuOptionButton({
icon: "file_upload",
label: "Import",
container: widget.root,
onclick: () => ppixiv.extraImageData.import()
});
new MenuOptionButton({
icon: "file_download",
label: "Export",
container: widget.root,
onclick: () => ppixiv.extraImageData.export()
});
return widget;
},
stageSlideshow: () => {
let widget = new MenuOptionRow({
...globalOptions,
label: "Bookmark slideshow"
});
new MenuOptionButton({
icon: "wallpaper",
label: "Go",
container: widget.root,
onclick: () => {
globalOptions.closeSettings();
SlideshowStagingDialog.show();
}
});
return widget;
},
quickView: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Quick view",
setting: "quick_view",
explanationEnabled: "Navigate to images immediately when the mouse button is pressed",
check: () => {
if (PointerListener.pointerType === "mouse")
return true;
ppixiv.message.show(
"Quick View is only supported when using a mouse."
);
return false;
}
});
},
autoPan: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Pan images",
setting: "auto_pan",
explanationEnabled: "Pan images while viewing them (drag the image to stop)"
});
},
autoPanSpeed: () => {
let button = new MenuOptionButton({
...globalOptions,
label: "Time per image",
getLabel: () => "Pan duration",
explanationEnabled: (value) => {
let seconds = ppixiv.settings.get("auto_pan_duration");
return \`\${seconds} \${seconds != 1 ? "seconds" : "second"}\`;
}
});
new MenuOptionSliderSetting({
container: button.querySelector(".widget-box"),
setting: "auto_pan_duration",
list: [1, 2, 3, 5, 10, 15, 20, 30, 45, 60],
classes: ["size-slider"],
refresh: () => button.refresh()
});
return button;
},
slideshowSpeed: () => {
let button = new MenuOptionButton({
...globalOptions,
label: "Time per image",
getLabel: () => "Slideshow duration",
explanationEnabled: (value) => {
let seconds = ppixiv.settings.get("slideshow_duration");
return \`\${seconds} \${seconds != 1 ? "seconds" : "second"}\`;
}
});
new MenuOptionSliderSetting({
container: button.querySelector(".widget-box"),
setting: "slideshow_duration",
list: [1, 2, 3, 5, 10, 15, 20, 30, 45, 60, 90, 120, 180],
classes: ["size-slider"],
refresh: () => {
button.refresh();
}
});
},
slideshowDefaultAnimation: () => {
return new MenuOptionOptionsSetting({
...globalOptions,
setting: "slideshow_default",
label: "Slideshow mode",
values: {
pan: "Pan",
contain: "Fade"
},
explanation: (value) => {
switch (value) {
case "pan":
return "Pan the image left-to-right or top-to-bottom";
case "contain":
return "Fade in and out without panning";
}
}
});
},
slideshowSkipsManga: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Slideshow skips manga pages",
setting: "slideshow_skips_manga",
explanationEnabled: "Slideshow mode will only show the first page.",
explanationDisabled: "Slideshow mode will show all pages."
});
},
displayMode: () => {
return new MenuOptionOptionsSetting({
...globalOptions,
setting: "display_mode",
label: "Display mode",
values: {
auto: "Automatic",
normal: "Fill the screen",
notch: "Rounded display",
safe: "Avoid the status bar"
}
});
},
expandMangaPosts: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Expand manga posts",
setting: "expand_manga_thumbnails"
});
},
viewMode: () => {
new MenuOptionToggleSetting({
...globalOptions,
label: "Return to the top when changing images",
setting: "view_mode",
onValue: "manga",
offValue: "illust"
});
},
useOriginalDownloadFilename: () => {
new MenuOptionToggleSetting({
...globalOptions,
label: "Use original pixiv download filename like 123_p0.jpg",
setting: "use_original_download_filename",
onValue: "true",
offValue: "false"
});
},
preloadManga: () => {
let values = {
full: "All pages",
partial: "Nearby pages",
thumbnails: "Thumbnails only"
};
return new MenuOptionOptionsSetting({
...globalOptions,
setting: "preload_manga",
label: "Preload manga",
values
});
},
openPixiv: () => {
return new MenuOptionButton({
...globalOptions,
label: "Open Pixiv",
onclick: () => {
let url = new URL("#no-ppixiv", window.location);
window.open(url);
}
});
},
linkTabs: () => {
let widget = new LinkTabsPopup({
...globalOptions
});
globalOptions.pageRemovedSignal.addEventListener("abort", () => {
widget.visible = false;
});
return widget;
},
enableLinkedTabs: () => {
return new MenuOptionToggleSetting({
...globalOptions,
label: "Enabled",
setting: "linkedTabs_enabled"
});
},
unlinkAllTabs: () => {
return new MenuOptionButton({
...globalOptions,
label: "Unlink all tabs",
onclick: () => {
ppixiv.settings.set("linked_tabs", []);
}
});
},
mutedTags: () => {
let widget = new EditMutedTagsWidget({
muteType: "tag",
...globalOptions
});
globalOptions.pageRemovedSignal.addEventListener("abort", () => {
widget.visible = false;
});
return widget;
},
mutedUsers: () => {
let widget = new EditMutedTagsWidget({
muteType: "user",
...globalOptions
});
globalOptions.pageRemovedSignal.addEventListener("abort", () => {
widget.visible = false;
});
return widget;
},
whatsNew: async () => {
let widget = new WhatsNew({
...globalOptions
});
globalOptions.pageRemovedSignal.addEventListener("abort", () => {
widget.visible = false;
});
return widget;
},
nativeLogin: () => {
return new MenuOptionButton({
...globalOptions,
label: LocalAPI.localInfo.logged_in ? "Log out" : "Login",
onclick: async () => {
let { logged_in } = LocalAPI.localInfo;
if (!logged_in) {
LocalAPI.redirectToLogin();
return;
}
let prompt = new ConfirmPrompt({ header: "Log out?" });
let result = await prompt.result;
console.log(result);
if (result)
LocalAPI.logout();
}
});
}
};
}
var pageTitles = {
thumbnail: "Thumbnail options",
image: "Image viewing",
tagMuting: "Muted tags",
userMuting: "Muted users",
linkedTabs: "Linked tabs",
translation: "Translation",
translationOverride: "Translation",
downloads: "Downloads",
other: "Other",
whatsNew: "What's New"
};
var SettingsDialog = class extends DialogWidget {
constructor({ showPage = "thumbnail", ...options } = {}) {
super({
...options,
dialogClass: "settings-dialog",
classes: ["settings-window"],
header: "Settings",
template: \`
<div class="sections vertical-scroller"></div>
<div class="items vertical-scroller"></div>
\`
});
this.phone = helpers.other.isPhone();
helpers.html.setClass(this.root, "phone", this.phone);
this._pageButtons = {};
this.root.querySelector(".items").hidden = this.phone;
this.addPages();
showPage ??= "thumbnail";
if (!this.phone)
this.showPage(showPage);
}
addPages() {
this._createPageButton("thumbnail");
this._createPageButton("image");
if (!ppixiv.native) {
this._createPageButton("tagMuting");
this._createPageButton("userMuting");
}
if (ppixiv.imageTranslations.supported)
this._createPageButton("translation");
if (ppixiv.sendImage.enabled)
this._createPageButton("linkedTabs");
this._createPageButton("downloads");
this._createPageButton("other");
this._createPageButton("whatsNew");
}
_createPageButton(name) {
let pageButton = this.createTemplate({
html: helpers.createBoxLink({
label: pageTitles[name],
classes: ["settings-page-button"]
})
});
pageButton.dataset.page = name;
pageButton.addEventListener("click", (e) => {
this.showPage(name);
});
this.root.querySelector(".sections").appendChild(pageButton);
this._pageButtons[name] = pageButton;
if (this.phone)
helpers.html.setClass(pageButton, "selected", true);
return pageButton;
}
showPage(settingsPage) {
if (this.phone) {
this._hideAndShowPageDialog(settingsPage);
return;
}
if (this._visiblePageName == settingsPage)
return;
if (this._pageWidget != null)
this._pageWidget.shutdown();
console.assert(this._pageWidget == null);
this._visiblePageName = settingsPage;
if (settingsPage == null)
return;
this._pageWidget = this._createPage(settingsPage);
helpers.html.setClass(this._pageButtons[settingsPage], "selected", true);
if (!this.phone)
this.header = pageTitles[settingsPage];
this._pageWidget.shutdownSignal.addEventListener("abort", () => {
this._pageWidget = null;
helpers.html.setClass(this._pageButtons[settingsPage], "selected", false);
});
}
async _hideAndShowPageDialog(settingsPage) {
this.visible = false;
await this.visibilityChangePromise();
let dialog = new SettingsPageDialog({ settingsPage });
dialog.shutdownSignal.addEventListener("abort", () => {
new SettingsDialog();
});
}
_createPage(settingsPage) {
let pageWidget = new Widget({
container: this.root.querySelector(".items"),
template: \`
<div class=settings-page></div>
\`
});
SettingsDialog._fillPage({
settingsPage,
pageWidget,
pageContainer: pageWidget.root
});
return pageWidget;
}
static _fillPage({ settingsPage, pageWidget, pageContainer }) {
let isSettingsList = settingsPage != "tagMuting" && settingsPage != "userMuting";
if (isSettingsList)
pageContainer.classList.add("settings-list");
let globalOptions = {
classes: ["settings-row"],
container: pageContainer,
pageRemovedSignal: pageWidget.shutdownSignal,
closeSettings: () => {
this.visible = false;
}
};
let settingsWidgets = createSettingsWidget({ globalOptions });
let pages = {
thumbnail: () => {
settingsWidgets.thumbnailSize();
settingsWidgets.thumbnailStyle();
if (!ppixiv.mobile) {
settingsWidgets.disableThumbnailPanning();
settingsWidgets.disableThumbnailZooming();
settingsWidgets.quickView();
settingsWidgets.uiOnHover();
}
if (!ppixiv.native)
settingsWidgets.expandMangaPosts();
},
image: () => {
settingsWidgets.autoPan();
settingsWidgets.autoPanSpeed();
settingsWidgets.slideshowSpeed();
settingsWidgets.slideshowDefaultAnimation();
if (!ppixiv.native)
settingsWidgets.slideshowSkipsManga();
if (ppixiv.mobile)
settingsWidgets.displayMode();
settingsWidgets.viewMode();
if (!ppixiv.mobile) {
settingsWidgets.invertScrolling();
settingsWidgets.noHideCursor();
settingsWidgets.showMangaPageIndicator();
}
},
tagMuting: () => {
settingsWidgets.mutedTags();
},
userMuting: () => {
settingsWidgets.mutedUsers();
},
linkedTabs: () => {
settingsWidgets.linkTabs();
settingsWidgets.unlinkAllTabs();
},
translation: () => {
createTranslationSettingsWidgets({
globalOptions,
editOverrides: false
});
if (!ppixiv.mobile) {
settingsWidgets.enableTransitions();
}
},
translationOverride: () => createTranslationSettingsWidgets({
globalOptions,
editOverrides: true
}),
downloads: () => settingsWidgets.useOriginalDownloadFilename(),
other: () => {
if (ppixiv.native && !LocalAPI.localInfo.local)
settingsWidgets.nativeLogin();
settingsWidgets.disableTranslations();
if (!ppixiv.native && !ppixiv.mobile)
settingsWidgets.disabledByDefault();
if (!ppixiv.mobile) {
if (navigator.userAgent.indexOf("Firefox/") === -1)
settingsWidgets.invertPopupHotkey();
settingsWidgets.ctrlOpensPopup();
}
settingsWidgets.bookmarkPrivatelyByDefault();
if (!ppixiv.mobile)
settingsWidgets.limitSlideshowFramerate();
if (!ppixiv.native)
settingsWidgets.preloadManga();
if (!ppixiv.native && ppixiv.mobile)
settingsWidgets.openPixiv();
if (!ppixiv.native && window.showOpenFilePicker != null)
settingsWidgets.importExtraData();
if (ppixiv.native || !ppixiv.mobile)
settingsWidgets.stageSlideshow();
if (!ppixiv.native)
settingsWidgets.showAiIcon();
if (!ppixiv.native || !ppixiv.mobile)
settingsWidgets.touchpadMode();
},
whatsNew: () => {
settingsWidgets.whatsNew();
}
};
let createPage = pages[settingsPage];
if (createPage == null) {
console.error(\`Invalid settings page: \${settingsPage}\`);
return;
}
createPage();
for (let boxLink of pageContainer.querySelectorAll(
".settings-page > .box-link"
))
boxLink.classList.add("allow-wrap");
}
};
var SettingsPageDialog = class extends DialogWidget {
constructor({ settingsPage, ...options } = {}) {
super({
header: pageTitles[settingsPage],
...options,
dialogClass: "settings-dialog-page",
backIcon: true,
template: \`\`
});
this._settingsContainer = this.querySelector(".scroll");
this._settingsContainer.classList.add("settings-page");
SettingsDialog._fillPage({
settingsPage,
pageWidget: this,
pageContainer: this._settingsContainer
});
}
};
var SlideshowStagingDialog = class extends DialogWidget {
static show() {
let slideshowArgs = ppixiv.app.slideshowURL;
if (slideshowArgs == null)
return;
let url = slideshowArgs.toString();
window.slideshowStagingDialog = window.open(url);
}
constructor({ ...options } = {}) {
let text = ppixiv.mobile ? \`
Add this page to your home screen for a slideshow of the current search.
\` : \`
Install this page as an app for a slideshow bookmark for the current search.
\`;
super({
...options,
showCloseButton: false,
header: "Slideshow",
template: \`
<div class=items style="font-size: 1.5rem; text-align: center;">
\${text}
</div>
\`
});
this.url = helpers.args.location;
document.title = window.opener.document.title;
window.addEventListener("appinstalled", (e) => {
if (ppixiv.mobile)
return;
window.opener.slideshowStagingDialog = null;
window.location.reload();
});
this.shutdownSignal.addEventListener("abort", () => window.close());
}
visibilityChanged() {
super.visibilityChanged();
if (!this.visible) {
if (helpers.args.location.toString() == this.url.toString())
ppixiv.phistory.back();
}
}
};
/\x2f web/vview/widgets/tag-list-widget.js
var TagListWidget = class extends Widget {
constructor({ ...options }) {
super({
...options,
template: \`
<div class="tag-list box-button-row"></div>
\`
});
}
formatTagLink(tag) {
return helpers.getArgsForTagSearch(tag, ppixiv.plocation);
}
async set(mediaInfo) {
this.mediaInfo = mediaInfo;
this.refresh();
}
async refresh() {
if (this.mediaInfo == null)
return;
let tags = [];
let showR18 = this.mediaInfo == 1;
let showR18G = this.mediaInfo == 1;
for (let tag of this.mediaInfo.tagList) {
if (tag == "R-18")
showR18 = true;
else if (tag == "R-18G")
showR18G = true;
else
tags.push({ tag });
}
let showAI = this.mediaInfo.aiType == 2;
if (showAI)
tags.splice(0, 0, { ai: true });
if (showR18G)
tags.splice(0, 0, { tag: "R-18G" });
else if (showR18)
tags.splice(0, 0, { tag: "R-18" });
if (this._currentTags != null && JSON.stringify(this._currentTags) == JSON.stringify(tags))
return;
let tagList = tags;
let translatedTags = await ppixiv.tagTranslations.getTranslations(this.mediaInfo.tagList, "en");
if (tagList != tags)
return;
this._currentTags = tags;
helpers.html.removeElements(this.root);
for (let { tag, ai } of tagList) {
if (ai)
tag = "AI-generated";
let translatedTag = tag;
if (translatedTags[tag])
translatedTag = translatedTags[tag];
let link = this.formatTagLink(tag);
if (ai)
link = null;
let a = helpers.createBoxLink({
label: translatedTag,
classes: ["tag-entry"],
link,
asElement: true
});
this.root.appendChild(a);
a.dataset.tag = tag;
}
}
};
/\x2f web/vview/misc/media-ids.js
function getUrlForMediaId(mediaId, { manga = false } = {}) {
if (helpers.mediaId.isLocal(mediaId)) {
let args2 = helpers.args.location;
LocalAPI.getArgsForId(mediaId, args2);
args2.hash.set("view", "illust");
return args2;
}
let [illustId, page] = helpers.mediaId.toIllustIdAndPage(mediaId);
let args = new helpers.args("/", ppixiv.plocation);
args.path = \`/artworks/\${illustId}\`;
if (manga)
args.hash.set("manga", "1");
if (page != null && page > 0)
args.hash.set("page", page + 1);
return args;
}
/\x2f web/vview/screen-illust/desktop-image-info.js
var DesktopImageInfo = class extends Widget {
constructor({ ...options }) {
super({
...options,
visible: false,
template: \`
<div class=image-ui>
<div class=hover-sphere>
<svg viewBox="0 0 1 1" preserveAspectRatio="none">
<circle class=hover-circle cx="0.5" cy="0.5" r=".5" fill-opacity="0" />
</svg>
</div>
<div class=ui-box>
<div class=avatar-popup></div>
<div class=ui-title-box>
<div>
<span class="title-block">
<!-- Put the title and author in separate inline-blocks, to encourage
the browser to wrap between them if possible, putting the author
on its own line if they won't both fit, but still allowing the
title to wrap if it's too long by itself. -->
<span style="display: inline-block;" class="title-font">
<a class="title"></a>
</span>
<span style="display: inline-block;" class="author-block title-font">
<span style="font-size: 12px;">by</span>
<a class="author" data-scroll-to-top></a>
</span>
<span style="display: inline-block;" class=folder-block>
<span style="font-size: 12px;">in</span>
<a class="folder-text title-font"></a>
</span>
<a class=edit-post href=#>Edit post</a>
</span>
</div>
</div>
<div class=button-row style='padding-block-end: 14px;'>
<a class="icon-button disable-ui-button popup pixiv-only" data-popup="Return to Pixiv" href="#no-ppixiv">
\${helpers.createIcon("ppixiv:pixiv")}
</a>
<div class="view-manga-button popup" data-popup="View manga pages">
<div class="icon-button">
\${helpers.createIcon("ppixiv:thumbnails")}
</div>
</div>
<div class="download-button download-image-button popup pixiv-only" data-download="image" data-popup="Download image">
<div class="icon-button button enabled">
<ppixiv-inline src="resources/download-icon.svg"></ppixiv-inline>
</div>
</div>
<div class="download-button download-manga-button popup pixiv-only" data-download="ZIP" data-popup="Download ZIP of all images">
<div class="icon-button button enabled">
<ppixiv-inline src="resources/download-manga-icon.svg"></ppixiv-inline>
</div>
</div>
<div class="download-button download-video-button popup pixiv-only" data-download="MKV" data-popup="Download MKV">
<div class="icon-button button enabled">
<ppixiv-inline src="resources/download-icon.svg"></ppixiv-inline>
</div>
</div>
<vv-container class=bookmark-button-container data-bookmark-type=public></vv-container>
<vv-container class="bookmark-button-container button-container" data-bookmark-type=private></vv-container>
<div style="position: relative;">
<div class="button icon-button button-bookmark-tags popup" data-popup="Bookmark tags">
\${helpers.createIcon("ppixiv:tag")}
<div style="position: absolute; bottom: 2px; left: 4px;">
<div class=tag-dropdown-arrow hidden></div>
</div>
</div>
</div>
<vv-container class="button-like-container button-container"></vv-container>
<a class="similar-illusts-button popup pixiv-only" data-popup="Similar illustrations" href=# hidden>
<div class=icon-button>
\${helpers.createIcon("ppixiv:suggestions")}
</div>
</a>
<a class="similar-artists-button popup pixiv-only" data-popup="Similar artists" href=# hidden>
<div class=icon-button>
\${helpers.createIcon("ppixiv:suggestions")}
</div>
</a>
<a class="similar-bookmarks-button popup pixiv-only" data-popup="Similar bookmarks" href=# hidden>
<div class=icon-button>
\${helpers.createIcon("ppixiv:suggestions")}
</div>
</a>
<div class="image-settings-menu-box popup" data-popup="Preferences">
<div class="icon-button preferences-button">
\${helpers.createIcon("settings")}
</div>
</div>
</div>
<div class=post-info>
<div class="post-age popup" hidden></div>
<div class=image-info hidden></div>
<div class=page-count hidden></div>
<div class=ugoira-duration hidden></div>
<div class=ugoira-frames hidden></div>
</div>
<vv-container class=tag-list-container></vv-container>
<div class=description></div>
</div>
</div>
\`
});
this.ui_box = this.root.querySelector(".ui-box");
this.avatarWidget = new AvatarWidget({
container: this.root.querySelector(".avatar-popup"),
dropdownVisibilityChanged: () => {
this.refreshOverlayUiVisibility();
}
});
this.tagListWidget = new TagListWidget({
container: this.root.querySelector(".tag-list-container")
});
ppixiv.mediaCache.addEventListener("mediamodified", this.refresh, {
signal: this.shutdownSignal
});
this.likeButton = new LikeButtonWidget({
container: this.root.querySelector(".button-like-container"),
template: \`
<div class="button icon-button button-like enabled popup button-container" style="position: relative;">
<ppixiv-inline src="resources/like-button.svg"></ppixiv-inline>
</div>
\`
});
this.bookmarkButtons = [];
for (const a of this.root.querySelectorAll("[data-bookmark-type]")) {
this.bookmarkButtons.push(
new BookmarkButtonWidget({
container: a,
template: \`
<div class="button icon-button button-bookmark popup \${a.dataset.bookmarkType}">
<ppixiv-inline src="resources/\${a.dataset.bookmarkType === "private" ? "heart-icon-private" : "heart-icon"}.svg"></ppixiv-inline>
</div>
\`,
bookmarkType: a.dataset.bookmarkType
})
);
}
const bookmarkTagsButton = this.root.querySelector(".button-bookmark-tags");
this.bookmarkTagsDropdownOpener = new BookmarkTagDropdownOpener({
parent: this,
bookmarkTagsButton,
bookmarkButtons: this.bookmarkButtons,
onVisibilityChanged: () => {
this.refreshOverlayUiVisibility();
}
});
for (let button of this.root.querySelectorAll(".download-button"))
button.addEventListener("click", this.clickedDownload);
this.root.querySelector(".download-manga-button").addEventListener("click", this.clickedDownload);
this.root.querySelector(".view-manga-button").addEventListener("click", (e) => {
if (this.mediaId == null)
return;
let args = getUrlForMediaId(this.mediaId, { manga: true });
helpers.navigate(args);
});
this.ui_box.addEventListener(
"wheel",
(e) => {
if (this.ui_box.scrollHeight > this.ui_box.offsetHeight)
e.stopPropagation();
},
{ passive: false }
);
this.root.querySelector(".preferences-button").addEventListener("click", (e) => {
new SettingsDialog();
});
this.ui_box.addEventListener("mouseenter", (e) => {
this.hoveringOverBox = true;
this.refreshOverlayUiVisibility();
});
this.ui_box.addEventListener("mouseleave", (e) => {
this.hoveringOverBox = false;
this.refreshOverlayUiVisibility();
});
let hoverCircle = this.querySelector(".hover-circle");
hoverCircle.addEventListener("mouseenter", (e) => {
this.hoveringOverSphere = true;
this.refreshOverlayUiVisibility();
});
hoverCircle.addEventListener("mouseleave", (e) => {
this.hoveringOverSphere = false;
this.refreshOverlayUiVisibility();
});
ppixiv.settings.addEventListener("image_editing", () => {
this.refreshOverlayUiVisibility();
});
ppixiv.settings.addEventListener("image_editing_mode", () => {
this.refreshOverlayUiVisibility();
});
ClassFlags.get.addEventListener(
"hide-ui",
() => this.refreshOverlayUiVisibility(),
this._signal
);
this.refreshOverlayUiVisibility();
}
refreshOverlayUiVisibility() {
let visible = this.hoveringOverBox || this.hoveringOverSphere;
let editing = ppixiv.settings.get("image_editing") && ppixiv.settings.get("image_editing_mode") != null;
if (editing)
visible = false;
if (this.bookmarkTagsDropdownOpener?.visible || this.avatarWidget.followDropdownOpener.visible)
visible = true;
if (ClassFlags.get.get("hide-ui"))
visible = false;
this.visible = visible;
this.root.hidden = editing || ppixiv.mobile;
}
applyVisibility() {
helpers.html.setClass(
this.root.querySelector(".ui-box"),
"ui-hidden",
!this._visible
);
}
visibilityChanged() {
super.visibilityChanged();
this.refresh();
}
set dataSource(dataSource) {
if (this._dataSource == dataSource)
return;
this._dataSource = dataSource;
this.refresh();
}
get mediaId() {
return this._mediaId;
}
set mediaId(mediaId) {
if (this._mediaId == mediaId)
return;
this._mediaId = mediaId;
this.refresh();
}
get displayedPage() {
return helpers.mediaId.parse(this._mediaId).page;
}
handleKeydown(e) {
}
refresh = async () => {
helpers.html.setClass(this.root, "disabled", !this.visible);
if (!this.visible)
return;
this.likeButton.setMediaId(this._mediaId);
for (let button of this.bookmarkButtons)
button.setMediaId(this._mediaId);
this.bookmarkTagsDropdownOpener.setMediaId(this._mediaId);
if (this._mediaId == null)
return;
this._setPostInfo();
let mediaId = this._mediaId;
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(this._mediaId);
if (mediaInfo == null || mediaId != this._mediaId || !this.visible)
return;
this._setPostInfo();
};
_setPostInfo() {
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(this._mediaId, {
full: false,
safe: false
});
if (mediaInfo == null)
return;
let [illustId] = helpers.mediaId.toIllustIdAndPage(this._mediaId);
let userId = mediaInfo.userId;
let ourPost = ppixiv.pixivInfo?.userId == userId;
this.querySelector(".author-block").hidden = ourPost;
this.querySelector(".edit-post").hidden = !ourPost;
this.querySelector(".edit-post").href = "/member_illust_mod.php?mode=mod&illust_id=" + illustId;
let disableButton = this.querySelector(".disable-ui-button");
disableButton.href = \`/artworks/\${illustId}#no-ppixiv\`;
this.avatarWidget.setUserId(userId);
this.avatarWidget.visible = userId != null;
this.tagListWidget.set(mediaInfo);
let elementTitle = this.root.querySelector(".title");
elementTitle.textContent = mediaInfo.illustTitle;
elementTitle.href = getUrlForMediaId(this._mediaId).url;
let folderTextElement = this.root.querySelector(".folder-text");
let showFolder = helpers.mediaId.isLocal(this._mediaId);
if (showFolder) {
let { id } = helpers.mediaId.parse(this.mediaId);
folderTextElement.innerText = helpers.strings.getPathSuffix(id, 2, 1);
let parentFolderId = LocalAPI.getParentFolder(id);
let args = new helpers.args("/", ppixiv.plocation);
LocalAPI.getArgsForId(parentFolderId, args);
folderTextElement.href = args.url;
}
this.root.querySelector(".author-block").hidden = mediaInfo.userName == "";
this.root.querySelector(".folder-block").hidden = !showFolder;
let elementAuthor = this.root.querySelector(".author");
elementAuthor.href = \`/users/\${userId}#ppixiv\`;
if (mediaInfo.userName != "")
elementAuthor.textContent = mediaInfo.userName;
this.root.querySelector(".similar-illusts-button").href = "/bookmark_detail.php?illust_id=" + illustId + "#ppixiv?recommendations=1";
this.root.querySelector(".similar-artists-button").href = "/discovery/users#ppixiv?user_id=" + userId;
this.root.querySelector(".similar-bookmarks-button").href = "/bookmark_detail.php?illust_id=" + illustId + "#ppixiv";
let elementComment = this.root.querySelector(".description");
elementComment.hidden = !mediaInfo.full || mediaInfo.illustComment == "";
if (ppixiv.native)
elementComment.innerText = mediaInfo.full ? mediaInfo.illustComment : "";
else
elementComment.innerHTML = mediaInfo.full ? mediaInfo.illustComment : "";
helpers.pixiv.fixPixivLinks(elementComment);
if (!ppixiv.native)
helpers.pixiv.makePixivLinksInternal(elementComment);
let downloadImageButton = this.root.querySelector(".download-image-button");
downloadImageButton.hidden = !Actions.isDownloadTypeAvailable(
"image",
mediaInfo
);
let downloadMangaButton = this.root.querySelector(".download-manga-button");
downloadMangaButton.hidden = !Actions.isDownloadTypeAvailable(
"ZIP",
mediaInfo
);
let downloadVideoButton = this.root.querySelector(".download-video-button");
downloadVideoButton.hidden = !Actions.isDownloadTypeAvailable(
"MKV",
mediaInfo
);
let postInfoContainer = this.root.querySelector(".post-info");
let setInfo = (query, text) => {
let node = postInfoContainer.querySelector(query);
node.innerText = text;
node.hidden = text == "";
};
let seconds_old = (new Date() - new Date(mediaInfo.createDate)) / 1e3;
setInfo(".post-age", helpers.strings.ageToString(seconds_old));
postInfoContainer.querySelector(".post-age").dataset.popup = helpers.strings.dateToString(mediaInfo.createDate);
let info = "";
if (this.displayedPage != null && mediaInfo.full) {
let pageInfo = mediaInfo.mangaPages[this.displayedPage];
info += pageInfo.width + "x" + pageInfo.height;
if (mediaInfo.illustType != 2) {
let url = new URL(pageInfo.urls?.original);
let ext = helpers.strings.getExtension(url.pathname).toUpperCase();
if (ext)
info += " " + ext;
}
}
setInfo(".image-info", info);
let duration = "";
if (mediaInfo.full && mediaInfo.ugoiraMetadata) {
let seconds = 0;
for (let frame of mediaInfo.ugoiraMetadata.frames)
seconds += frame.delay / 1e3;
duration = seconds.toFixed(duration >= 10 ? 0 : 1);
duration += seconds == 1 ? " second" : " seconds";
}
setInfo(".ugoira-duration", duration);
setInfo(
".ugoira-frames",
mediaInfo.full && mediaInfo.ugoiraMetadata ? mediaInfo.ugoiraMetadata.frames.length + " frames" : ""
);
let pageText = "";
if (mediaInfo.pageCount > 1 && this.displayedPage != null)
pageText = "Page " + (this.displayedPage + 1) + "/" + mediaInfo.pageCount;
setInfo(".page-count", pageText);
}
clickedDownload = (e) => {
if (this._mediaId == null)
return;
let clickedButton = e.target.closest(".download-button");
if (clickedButton == null)
return;
e.preventDefault();
e.stopPropagation();
let downloadType = clickedButton.dataset.download;
Actions.downloadIllust(this._mediaId, downloadType, this.displayedPage);
};
};
/\x2f web/vview/actors/direct-animation.js
var DirectAnimation = class {
constructor(effect, {
limitFramerate = true
} = {}) {
this._limitFramerate = limitFramerate;
this.animation = new Animation(effect);
this._updatePlayState("idle");
}
get effect() {
return this.animation.effect;
}
_updatePlayState(state) {
if (state == this._playState)
return;
if (this.finished == null || this._playState == "finished") {
this.finished = helpers.other.makePromise();
this.finished.catch((f) => true);
}
this._playState = state;
}
play() {
if (this._playState == "running")
return;
this._updatePlayState("running");
this._playToken = new Object();
this._runner = this._runAnimation();
}
pause() {
if (this._playState == "paused")
return;
this._updatePlayState("paused");
this._playToken = null;
this._runner = null;
}
cancel() {
this.pause();
this.animation.cancel();
}
updatePlaybackRate(rate) {
return this.animation.updatePlaybackRate(rate);
}
commitStyles() {
this.animation.commitStyles();
}
commitStylesIfPossible() {
try {
this.commitStyles();
return true;
} catch (e) {
console.error(e);
return false;
}
}
get playState() {
return this._playState;
}
get currentTime() {
return this.animation.currentTime;
}
async _runAnimation() {
this.animation.currentTime = this.animation.currentTime;
let token = this._playToken;
let lastUpdate = Date.now();
if (this.animation.currentTime == null)
this.animation.currentTime = 0;
while (1) {
let delta;
while (1) {
await helpers.other.vsync();
if (token !== this._playToken) {
this.finished.reject(new DOMException("The animation was aborted", "AbortError"));
return;
}
let now = Date.now();
delta = now - lastUpdate;
if (this._limitFramerate && !ppixiv.mobile) {
let targetFramerate = ppixiv.settings.get("slideshow_framerate");
if (targetFramerate != null) {
let targetDelay = 1e3 / targetFramerate;
if (delta * 1.05 < targetDelay)
continue;
}
}
lastUpdate = now;
break;
}
delta *= this.animation.playbackRate;
let newCurrentTime = this.animation.currentTime + delta;
let timing = this.animation.effect.getComputedTiming();
let maxTime = timing.duration * timing.iterations;
let finished = newCurrentTime >= maxTime;
if (finished)
newCurrentTime = maxTime;
this.animation.currentTime = newCurrentTime;
if (finished) {
this._updatePlayState("finished");
this.finished.accept();
if (this.onfinish)
this.onfinish();
break;
}
}
}
};
/\x2f web/vview/screen-illust/mobile-image-changer.js
var DragImageChanger = class extends Actor {
constructor({ parent }) {
super({ parent });
this.recentPointerMovement = new FlingVelocity({
samplePeriod: 0.15
});
this.dragDistance = 0;
this.viewers = [];
this.animations = null;
this.bounds = [null, null];
this.dragger = new DragHandler({
parent: this,
name: "image-changer",
element: this.parent.root,
confirmDrag: ({ event }) => {
if (this.mainViewer == null)
return false;
if (helpers.shouldIgnoreHorizontalDrag(event))
return false;
return true;
},
ondragstart: (args) => this.ondragstart(args),
ondrag: (args) => this.ondrag(args),
ondragend: (args) => this.ondragend(args),
deferredStart: () => {
return this.animations == null && this.dragDistance == 0;
}
});
}
get viewerDistance() {
return this.parent.viewContainer.offsetWidth + this.imageGap;
}
get imageGap() {
return 25;
}
get mainViewer() {
return this.parent.viewer;
}
stop() {
this.dragger.cancelDrag();
this.cancelAnimation();
this.removeViewers();
}
ondragstart({ event }) {
if (this.animations == null && this.dragDistance == 0 && Math.abs(event.movementY) > Math.abs(event.movementX))
return false;
this.dragDistance = 0;
this.recentPointerMovement.reset();
this.bounds = [null, null];
if (this.animations == null) {
this.viewers = [this.mainViewer];
return true;
}
this.cancelAnimation();
return true;
}
cancelAnimation() {
if (!this.animations)
return;
let animations = this.animations;
this.animations = null;
for (let animation of animations)
animation.pause();
if (this.dragDistance != null && this.mainViewer) {
let mainTransform = new DOMMatrix(getComputedStyle(this.mainViewer.root).transform);
this.dragDistance = mainTransform.e;
this.refreshDragPosition();
}
for (let animation of animations)
animation.cancel();
}
ondrag({ event, first }) {
let x = event.movementX;
this.recentPointerMovement.addSample({ x });
let overscroll = 1;
if (this.bounds[0] != null && this.dragDistance > this.bounds[0]) {
let distance2 = Math.abs(this.bounds[0] - this.dragDistance);
overscroll = Math.pow(0.97, distance2);
}
if (this.bounds[1] != null && this.dragDistance < this.bounds[1]) {
let distance2 = Math.abs(this.bounds[1] - this.dragDistance);
overscroll = Math.pow(0.97, distance2);
}
x *= overscroll;
if (!first)
this.dragDistance += x;
this._addViewersIfNeeded();
this.refreshDragPosition();
}
getViewerX(viewerIndex) {
let relativeIdx = viewerIndex - this.mainViewerIndex;
let x = this.viewerDistance * relativeIdx;
x += this.dragDistance;
return x;
}
refreshDragPosition() {
for (let idx = 0; idx < this.viewers.length; ++idx) {
let viewer = this.viewers[idx];
let x = this.getViewerX(idx);
viewer.root.style.transform = \`translateX(\${x}px)\`;
viewer.visible = true;
}
}
get mainViewerIndex() {
return this._findViewerIndex(this.mainViewer);
}
_findViewerIndex(viewer) {
let index = this.viewers.indexOf(viewer);
if (index == -1) {
console.error("Viewer is missing");
return 0;
}
return index;
}
async _addViewersIfNeeded() {
let dragThreshold = 5;
let leftViewerEdge = this.getViewerX(-1) + this.viewerDistance;
let addForwards = null;
if (leftViewerEdge > dragThreshold)
addForwards = false;
let rightViewerEdge = this.getViewerX(this.viewers.length) - this.imageGap;
if (rightViewerEdge < window.innerWidth - dragThreshold)
addForwards = true;
let targetViewerIndex = this.currentDragTarget();
if (targetViewerIndex < 0)
addForwards = false;
else if (targetViewerIndex >= this.viewers.length)
addForwards = true;
if (addForwards == null)
return;
let neighborViewer = this.viewers[addForwards ? this.viewers.length - 1 : 0];
let neighborMediaId = neighborViewer.mediaId;
let { mediaId, earlyIllustData, cancelled } = await this._createViewer(addForwards, neighborMediaId);
if (cancelled) {
return;
}
if (mediaId == null) {
if (addForwards)
this.bounds[1] = this.viewerDistance * (this.viewers.length - 1 - this.mainViewerIndex);
else
this.bounds[0] = this.viewerDistance * (0 - this.mainViewerIndex);
return;
}
let viewer = this.parent.createViewer({
earlyIllustData,
mediaId,
displayedByDrag: true
});
viewer.visible = false;
this.viewers.splice(addForwards ? this.viewers.length : 0, 0, viewer);
this.refreshDragPosition();
}
async _createViewer(addForwards, neighborMediaId) {
let viewers = this.viewers;
let sentinel = this.addingViewer = new Object();
try {
let mediaId = await this.parent.getNavigation(addForwards, { navigateFromMediaId: neighborMediaId });
if (mediaId == null)
return {};
let earlyIllustData = await ppixiv.mediaCache.getMediaInfo(mediaId, { full: false });
return { mediaId, earlyIllustData };
} finally {
let cancelled = sentinel != this.addingViewer;
if (sentinel == this.addingViewer)
this.addingViewer = null;
if (this.viewers !== viewers)
cancelled = true;
if (cancelled)
return { cancelled: true };
}
}
removeViewers() {
for (let viewer of this.viewers) {
if (viewer != this.mainViewer)
viewer.shutdown();
}
this.viewers = [];
}
currentDragTarget() {
let recentVelocity = this.recentPointerMovement.currentVelocity.x;
let threshold = 200;
if (Math.abs(recentVelocity) > threshold) {
if (recentVelocity > threshold)
return this.mainViewerIndex - 1;
else if (recentVelocity < -threshold)
return this.mainViewerIndex + 1;
}
let closestViewreIndex = 0;
let closestViewerDistance = 999999;
for (let idx = 0; idx < this.viewers.length; ++idx) {
let x = this.getViewerX(idx);
let center = x + window.innerWidth / 2;
let distance2 = Math.abs(window.innerWidth / 2 - center);
if (distance2 < closestViewerDistance) {
closestViewerDistance = distance2;
closestViewreIndex = idx;
}
}
return closestViewreIndex;
}
async ondragend({ interactive, cancel } = {}) {
let draggedToViewer = null;
if (interactive && !cancel) {
let targetViewerIndex = this.currentDragTarget();
if (targetViewerIndex >= 0 && targetViewerIndex < this.viewers.length)
draggedToViewer = this.viewers[targetViewerIndex];
}
let recentVelocity = this.recentPointerMovement.currentVelocity.x;
this.recentPointerMovement.reset();
if (!interactive) {
this.dragDistance = 0;
this.cancelAnimation();
this.removeViewers();
return;
}
if (draggedToViewer) {
let oldMainIndex = this.mainViewerIndex;
let newMainIndex = this._findViewerIndex(draggedToViewer);
this.parent.latestNavigationDirectionDown = newMainIndex > oldMainIndex;
this.parent.takeViewer();
this.parent.showImageViewer({ newViewer: draggedToViewer });
let args = ppixiv.app.getMediaURL(draggedToViewer.mediaId);
helpers.navigate(args, { addToHistory: false, sendPopstate: false });
}
let duration = 400;
let animations = [];
let mainViewerIndex = this.mainViewerIndex;
for (let idx = 0; idx < this.viewers.length; ++idx) {
let viewer = this.viewers[idx];
let thisIdxd = idx - mainViewerIndex;
let startX = new DOMMatrix(getComputedStyle(viewer.root).transform).e;
let endX = this.viewerDistance * thisIdxd;
let { easing } = Bezier2D.findCurveForVelocity({
distance: Math.abs(endX - startX),
duration,
targetVelocity: Math.abs(recentVelocity)
});
if (endX > startX != recentVelocity > 0)
easing = "ease-out";
let animation = new DirectAnimation(new KeyframeEffect(viewer.root, [
{ transform: viewer.root.style.transform },
{ transform: \`translateX(\${endX}px)\` }
], {
duration,
fill: "forwards",
easing
}));
animation.play();
animations.push(animation);
}
this.dragDistance = 0;
this.animations = animations;
let animationsFinished = Promise.all(animations.map((animation) => animation.finished));
try {
await animationsFinished;
} catch (e) {
return;
}
console.assert(this.animations === animations);
this.animations = null;
for (let animation of animations) {
animation.commitStylesIfPossible();
animation.cancel();
}
this.removeViewers();
}
};
/\x2f web/vview/screen-illust/mobile-image-dismiss.js
var MobileImageDismiss = class extends Actor {
constructor({ parent }) {
super({ parent });
this.dragger = new WidgetDragger({
parent: this,
name: "drag-to-exit",
nodes: [
this.parent.root,
this.parent.querySelector(".fade-search")
],
dragNode: this.parent.root,
size: () => this._dragDistance,
animatedProperty: "--illust-hidden",
animatedPropertyInverted: true,
visible: false,
direction: "up",
duration: () => {
return ppixiv.settings.get("animations_enabled") ? 250 : 0;
},
confirmDrag: ({ event }) => this.parent._active && ppixiv.mobile,
onactive: () => {
if (this.parent.mobileIllustUi)
this.parent.mobileIllustUi.hide();
this._configAnimation();
},
oninactive: () => {
if (this.dragger.visible) {
this.scrollSearchToThumbnail();
} else {
if (this.parent._active) {
let args = new helpers.args(this.parent.dataSource.searchUrl.toString());
ppixiv.app.navigateFromIllustToSearch(args);
}
this.parent.cleanupImage();
}
}
});
}
get _dragDistance() {
return document.documentElement.clientHeight * 0.5;
}
_configAnimation() {
this.parent.root.style.setProperty("--animation-x", \`0px\`);
this.parent.root.style.setProperty("--animation-y", \`0px\`);
this.parent.root.style.setProperty("--animation-scale", "1");
let viewPosition = this.parent.viewer?.viewPosition;
if (viewPosition) {
let { left, top } = this.parent.viewer.root.getBoundingClientRect();
viewPosition.x += left;
viewPosition.y += top;
}
viewPosition ??= this.parent.root.getBoundingClientRect();
let thumbRect = this._animationTargetRect;
if (thumbRect) {
let center_y = thumbRect.top + thumbRect.height / 2;
if (center_y < 0 || center_y > window.innerHeight)
thumbRect = null;
}
if (thumbRect == null) {
let width2 = viewPosition.width * 0.75;
let height2 = viewPosition.height * 0.75;
let x2 = (window.innerWidth - width2) / 2;
let y2 = (window.innerHeight - height2) / 2;
thumbRect = new FixedDOMRect(x2, y2, x2 + width2, y2 + height2);
}
let { x, y, width, height } = viewPosition;
let scale2 = Math.max(thumbRect.width / width, thumbRect.height / height);
let animationX = -(x + width / 2) * scale2;
let animationY = -(y + height / 2) * scale2;
animationX += thumbRect.x + thumbRect.width / 2;
animationY += thumbRect.y + thumbRect.height / 2;
this.parent.root.style.setProperty("--animation-x", \`\${animationX}px\`);
this.parent.root.style.setProperty("--animation-y", \`\${animationY}px\`);
this.parent.root.style.setProperty("--animation-scale", scale2);
}
get _animationTargetRect() {
return ppixiv.app.getRectForMediaId(this.parent._wantedMediaId);
}
activate({ cause }) {
if (!this.dragger.visible || !this.dragger.isAnimatingToShown) {
let transition = cause != "initialization";
this.dragger.show({ transition });
if (!transition)
this.scrollSearchToThumbnail();
}
}
deactivate() {
if (this.dragger.visible)
this.dragger.hide();
}
get isAnimating() {
return this.dragger.isAnimationPlaying;
}
get waitForAnimationsPromise() {
return this.dragger.finished;
}
scrollSearchToThumbnail() {
if (this.isAnimating || !this.parent.active || this.dragger.position < 1)
return;
ppixiv.app.scrollSearchToMediaId(this.parent.dataSource, this.parent._wantedMediaId);
}
};
/\x2f web/vview/widgets/more-options-dropdown.js
var MoreOptionsDropdown = class extends IllustWidget {
get neededData() {
return "partial";
}
constructor({
showExtra = false,
...options
}) {
super({
...options,
template: \`
<div class="more-options-dropdown">
<div class="options vertical-list" style="min-width: 13em;"></div>
</div>
\`
});
this.showExtra = showExtra;
this._menuOptions = [];
}
_createMenuOptions() {
let optionBox = this.root.querySelector(".options");
let sharedOptions = {
container: optionBox,
parent: this
};
for (let item of this._menuOptions)
item.root.remove();
let menuOptions = {
similarIllustrations: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Similar illustrations",
icon: "ppixiv:suggestions",
requiresImage: true,
onclick: () => {
this.parent.hide();
let [illustId] = helpers.mediaId.toIllustIdAndPage(this.mediaId);
let args = new helpers.args(\`/bookmark_detail.php?illust_id=\${illustId}#ppixiv?recommendations=1\`);
helpers.navigate(args);
}
});
},
similarArtists: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Similar artists",
icon: "ppixiv:suggestions",
requiresUser: true,
onclick: () => {
this.parent.hide();
let args = new helpers.args(\`/discovery/users#ppixiv?user_id=\${this._effectiveUserId}\`);
helpers.navigate(args);
}
});
},
similarLocalImages: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Similar images",
icon: "ppixiv:suggestions",
requiresImage: true,
onclick: () => {
this.parent.hide();
let args = new helpers.args("/");
args.path = "/similar";
args.hashPath = "/#/";
let { id } = helpers.mediaId.parse(this.mediaId);
args.hash.set("search_path", id);
helpers.navigate(args);
}
});
},
similarBookmarks: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Similar bookmarks",
icon: "ppixiv:suggestions",
requiresImage: true,
onclick: () => {
this.parent.hide();
let [illustId] = helpers.mediaId.toIllustIdAndPage(this.mediaId);
let args = new helpers.args(\`/bookmark_detail.php?illust_id=\${illustId}#ppixiv\`);
helpers.navigate(args);
}
});
},
indexFolderForSimilaritySearch: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Index similarity",
icon: "ppixiv:suggestions",
hideIfUnavailable: true,
requires: ({ mediaId }) => {
if (mediaId == null)
return false;
let { type } = helpers.mediaId.parse(mediaId);
return type == "folder";
},
onclick: () => {
this.parent.hide();
LocalAPI.indexFolderForSimilaritySearch(this.mediaId);
}
});
},
toggleUpscaling: () => {
return new MenuOptionToggleSetting({
...sharedOptions,
label: "GPU upscaling",
icon: "mat:zoom_out_map",
requiresImage: true,
setting: "upscaling"
});
},
editMutes: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Edit mutes",
requires: ({ mediaId, userId }) => {
return mediaId != null || userId != null;
},
icon: "mat:block",
onclick: async () => {
this.parent.hide();
new MutedTagsForPostDialog({
mediaId: this.mediaId,
userId: this._effectiveUserId
});
}
});
},
refreshImage: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Refresh image",
requiresImage: true,
icon: "mat:refresh",
onclick: async () => {
this.parent.hide();
ppixiv.mediaCache.refreshMediaInfo(this.mediaId, { refreshFromDisk: true });
}
});
},
shareImage: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Share image",
icon: "mat:share",
requires: ({ mediaId }) => {
if (navigator.share == null)
return false;
if (mediaId == null || helpers.mediaId.isLocal(mediaId))
return false;
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
return mediaInfo && mediaInfo.illustType != 2;
},
onclick: async () => {
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(this._mediaId, { full: true });
let page = helpers.mediaId.parse(this.mediaId).page;
let { url } = mediaInfo.getMainImageUrl(page);
let title = \`\${mediaInfo.userName} - \${mediaInfo.illustId}\`;
if (mediaInfo.mangaPages.length > 1) {
let mangaPage = helpers.mediaId.parse(this._mediaId).page;
title += " #" + (mangaPage + 1);
}
title += \`.\${helpers.strings.getExtension(url)}\`;
navigator.share({
url,
title
});
}
});
},
downloadImage: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Download image",
icon: "mat:download",
hideIfUnavailable: true,
requiresImage: true,
available: () => {
return this.mediaInfo && Actions.isDownloadTypeAvailable("image", this.mediaInfo);
},
onclick: () => {
Actions.downloadIllust(this.mediaId, "image");
this.parent.hide();
}
});
},
downloadManga: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Download manga ZIP",
icon: "mat:download",
hideIfUnavailable: true,
requiresImage: true,
available: () => {
return this.mediaInfo && Actions.isDownloadTypeAvailable("ZIP", this.mediaInfo);
},
onclick: () => {
Actions.downloadIllust(this.mediaId, "ZIP");
this.parent.hide();
}
});
},
downloadVideo: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Download video MKV",
icon: "mat:download",
hideIfUnavailable: true,
requiresImage: true,
available: () => {
return this.mediaInfo && Actions.isDownloadTypeAvailable("MKV", this.mediaInfo);
},
onclick: () => {
Actions.downloadIllust(this.mediaId, "MKV");
this.parent.hide();
}
});
},
sendToTab: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Send to tab",
classes: ["button-send-image"],
icon: "mat:open_in_new",
requiresImage: true,
onclick: () => {
new SendImagePopup({ mediaId: this.mediaId });
this.parent.hide();
}
});
},
toggleSlideshow: () => {
return new MenuOptionToggle({
...sharedOptions,
label: "Slideshow",
icon: "mat:wallpaper",
requiresImage: true,
checked: helpers.args.location.hash.get("slideshow") == "1",
onclick: () => {
ppixiv.app.toggleSlideshow();
this.refresh();
}
});
},
toggleLoop: () => {
return new MenuOptionToggle({
...sharedOptions,
label: "Loop",
checked: helpers.args.location.hash.get("slideshow") == "loop",
icon: "mat:replay_circle_filled",
requiresImage: true,
hideIfUnavailable: true,
onclick: () => {
ppixiv.app.loopSlideshow();
this.refresh();
}
});
},
linkedTabs: () => {
let widget = new MenuOptionToggleSetting({
container: optionBox,
label: "Linked tabs",
setting: "linked_tabs_enabled",
icon: "mat:link"
});
new MenuOptionButton({
container: widget.root.querySelector(".checkbox"),
containerPosition: "beforebegin",
icon: "mat:settings",
classes: ["small-font"],
onclick: (e) => {
e.stopPropagation();
new SettingsPageDialog({ settingsPage: "linkedTabs" });
this.parent.hide();
return true;
}
});
return widget;
},
imageEditing: () => {
return new MenuOptionToggleSetting({
...sharedOptions,
label: "Image editing",
icon: "mat:brush",
setting: "image_editing",
requiresImage: true,
onclick: () => {
let enabled = ppixiv.settings.get("image_editing");
if (!enabled)
ppixiv.settings.set("image_editing_mode", null);
}
});
},
openSettings: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Settings",
icon: "mat:settings",
onclick: () => {
new SettingsDialog();
this.parent.hide();
}
});
},
exit: () => {
return new MenuOptionButton({
...sharedOptions,
label: "Return to Pixiv",
icon: "mat:logout",
url: "#no-ppixiv"
});
},
toggleTranslations: () => {
let isEnabled = () => {
if (this.mediaId == null || this.mediaInfo == null)
return false;
return this.mediaInfo.illustType != 2;
};
let widget = new MenuOptionToggleImageTranslation({
...sharedOptions,
requires: () => isEnabled(),
mediaId: this.mediaId,
label: "Translate",
icon: "mat:translate"
});
new MenuOptionButton({
container: widget.root.querySelector(".checkbox"),
containerPosition: "beforebegin",
icon: "mat:settings",
classes: ["small-font"],
onclick: (e) => {
e.stopPropagation();
if (!isEnabled())
return;
new SettingsPageDialog({ settingsPage: "translationOverride" });
this.parent.hide();
return true;
}
});
return widget;
}
};
let screenName = ppixiv.app.getDisplayedScreen();
this._menuOptions = [];
if (!ppixiv.native) {
this._menuOptions.push(menuOptions.similarIllustrations());
this._menuOptions.push(menuOptions.similarArtists());
if (this.showExtra)
this._menuOptions.push(menuOptions.similarBookmarks());
this._menuOptions.push(menuOptions.downloadImage());
this._menuOptions.push(menuOptions.downloadManga());
this._menuOptions.push(menuOptions.downloadVideo());
this._menuOptions.push(menuOptions.editMutes());
if (ppixiv.settings.get("show_share"))
this._menuOptions.push(menuOptions.shareImage());
} else {
this._menuOptions.push(menuOptions.similarLocalImages());
}
if (screenName == "illust" && ppixiv.imageTranslations.supported)
this._menuOptions.push(menuOptions.toggleTranslations());
if (ppixiv.sendImage.enabled) {
this._menuOptions.push(menuOptions.sendToTab());
this._menuOptions.push(menuOptions.linkedTabs());
}
if (screenName == "illust") {
this._menuOptions.push(menuOptions.toggleSlideshow());
this._menuOptions.push(menuOptions.toggleLoop());
}
if (!ppixiv.mobile)
this._menuOptions.push(menuOptions.imageEditing());
if (ppixiv.native) {
this._menuOptions.push(menuOptions.indexFolderForSimilaritySearch());
this._menuOptions.push(menuOptions.toggleUpscaling());
}
if (this.showExtra || ppixiv.native)
this._menuOptions.push(menuOptions.refreshImage());
if (ppixiv.mobile)
this._menuOptions.push(menuOptions.openSettings());
if (!ppixiv.native && !ppixiv.mobile)
this._menuOptions.push(menuOptions.exit());
window.vviewHooks?.dropdownMenuOptions?.({ moreOptionsDropdown: this, sharedOptions });
}
setUserId(userId) {
this._userId = userId;
this.refresh();
}
visibilityChanged() {
if (this.visible)
this.refresh();
}
get _effectiveUserId() {
return this._userId ?? this.mediaInfo?.userId;
}
async refreshInternal({ mediaId, mediaInfo }) {
if (!this.visible)
return;
this._createMenuOptions();
this.mediaInfo = mediaInfo;
for (let option of this._menuOptions) {
let enable = true;
if (option.options.requiresImage && mediaId == null)
enable = false;
if (option.options.requiresUser && this._effectiveUserId == null)
enable = false;
if (option.options.requires && !option.options.requires({ mediaId, userId: this._effectiveUserId }))
enable = false;
if (enable && option.options.available)
enable = option.options.available();
option.enabled = enable;
if (option.options.hideIfUnavailable)
option.root.hidden = !enable;
}
}
};
/\x2f web/vview/actors/isolated-tap-handler.js
var _IsolatedTapHandler = class extends Actor {
static preventTaps() {
for (let handler of _IsolatedTapHandler.handlers) {
handler._clearPresses();
}
}
constructor({ parent, node, callback, delay = 350 } = {}) {
super({ parent });
this._node = node;
this._callback = callback;
this._lastPointerDownAt = -99999;
this._delay = delay;
this._timeoutId = -1;
this._pressed = false;
this._allPresses = /* @__PURE__ */ new Set();
_IsolatedTapHandler.handlers.add(this);
this.shutdownSignal.addEventListener("abort", () => _IsolatedTapHandler.handlers.delete(this));
this._eventNamesDuringTouch = ["pointerup", "pointercancel", "pointermove", "blur", "dblclick"];
this._node.addEventListener("pointerdown", this._handleEvent, this._signal);
}
_registerEvents() {
for (let type of this._eventNamesDuringTouch)
window.addEventListener(type, this._handleEvent, { capture: true, ...this._signal });
}
_unregisterEvents() {
for (let type of this._eventNamesDuringTouch)
this._node.removeEventListener(type, this._handleEvent, { capture: true });
}
_handleEvent = (e) => {
if (e.type == "blur") {
this._clearPresses();
return;
}
if (e.type == "pointerdown")
this._allPresses.add(e.pointerId);
else if (e.type == "pointerup" || e.type == "pointercancel")
this._allPresses.delete(e.pointerId);
if (this._pressed && e.pointerId != this._pressEvent.pointerId) {
this._unqueueEvent();
return;
}
if (e.type == "dblclick") {
this._unqueueEvent();
}
if (e.type == "pointercancel") {
this._clearPresses();
return;
}
if (e.type == "pointerdown") {
if (this._allPresses.size > 1) {
return;
}
this._registerEvents();
this._unqueueEvent();
let now = Date.now();
let timeSinceLastPress = now - this._lastPointerDownAt;
this._lastPointerDownAt = Date.now();
if (timeSinceLastPress < this._delay) {
return;
}
this._checkEvents = [];
this._pressed = true;
this._pressEvent = e;
this._queueEvent();
}
if (e.type == "pointermove") {
this._unqueueEvent();
return;
}
if (e.type == "pointerup") {
this._unregisterEvents();
this._pressed = false;
}
this._checkEvents.push(e);
};
_clearPresses() {
this._unqueueEvent();
this._allPresses.clear();
this._pressed = false;
}
_queueEvent = () => {
if (this._timeoutId != -1)
return;
this._timeoutId = realSetTimeout(() => {
if (this.hasShutdown)
return;
this._timeoutId = -1;
if (this._pressed) {
return;
}
for (let event of this._checkEvents) {
if (event.defaultPrevented || event.cancelBubble) {
return;
}
if (event.partiallyHandled) {
return;
}
}
let pressEvent = this._pressEvent;
this._pressEvent = null;
realSetTimeout(() => {
if (this.hasShutdown)
return;
this._callback(pressEvent);
}, 0);
}, this._delay);
};
_unqueueEvent = () => {
if (this._timeoutId == -1)
return;
realClearTimeout(this._timeoutId);
this._timeoutId = -1;
};
};
var IsolatedTapHandler = _IsolatedTapHandler;
__publicField(IsolatedTapHandler, "handlers", /* @__PURE__ */ new Set());
/\x2f web/vview/screen-illust/mobile-ui.js
var MobileImageUI = class extends Widget {
constructor({
transitionTarget,
...options
}) {
super({
...options,
template: \`
<div class=mobile-illust-ui-container>
<div class=mobile-illust-ui-page>
<div class=avatar></div>
<div class=menu-bar>
<div class="item button-info enabled">
\${helpers.createIcon("mat:info")}
<span class=label>Info</span>
</div>
<vv-container class="bookmark-button-container"></vv-container>
<vv-container class="manga-button-container"></vv-container>
<div class="item button-more enabled">
\${helpers.createIcon("settings")}
<span class=label>More...</span>
</div>
</div>
</div>
</div>
\`
});
this.transitionTarget = transitionTarget;
this.avatarWidget = new AvatarWidget({
container: this.root.querySelector(".avatar"),
clickAction: "author"
});
this.root.querySelector(".avatar").hidden = ppixiv.native;
this.getMediaInfo = new GetMediaInfo({
parent: this,
neededData: "partial",
onrefresh: async ({ mediaInfo }) => {
this.avatarWidget.visible = mediaInfo != null;
this.avatarWidget.setUserId(mediaInfo?.userId);
}
});
this.dragger = new WidgetDragger({
parent: this,
name: "menu-dragger",
nodes: [this.transitionTarget],
dragNode: this.root.parentNode,
size: () => this.querySelector(".menu-bar").offsetHeight,
animatedProperty: "--menu-bar-pos",
direction: "up",
confirmDrag: () => {
if (this.parent.mobileImageDismiss.isAnimating)
return false;
return true;
},
oncancelled: ({ otherDragger }) => {
if (!this.dragger.visible)
return;
this.dragger.hide();
IsolatedTapHandler.preventTaps();
},
onbeforeshown: () => this.callVisibilityChanged(),
onafterhidden: () => this.callVisibilityChanged(),
onactive: () => this.callVisibilityChanged(),
oninactive: () => this.callVisibilityChanged()
});
this._mediaId = null;
this.querySelector(".button-more").addEventListener("click", (e) => {
new MoreOptionsDialog({
mediaId: this._mediaId
});
this.dragger.hide();
});
this.querySelector(".button-info").addEventListener("click", (e) => {
new MobileIllustInfoDialog({
mediaId: this._mediaId,
dataSource: this.dataSource
});
this.dragger.hide();
});
this.buttonBookmark = this.querySelector(".bookmark-button-container");
this.bookmarkButtonWidget = new ImageBookmarkedWidget({
container: this.buttonBookmark
});
this.viewManga = this.querySelector(".manga-button-container");
this.viewMangaWidget = new ViewMangaWidget({ container: this.viewManga });
this.buttonBookmark.addEventListener("click", (e) => {
new BookmarkTagDialog({
mediaId: this._mediaId
});
this.dragger.hide();
});
helpers.html.setSizeAsProperty(this.querySelector(".menu-bar"), {
target: this.closest(".screen"),
heightProperty: "--menu-bar-height",
...this._signal
});
this.refresh();
}
set mediaId(mediaId) {
if (this._mediaId == mediaId)
return;
this._mediaId = mediaId;
this.refresh();
}
get mediaId() {
return this._mediaId;
}
get visible() {
return this.dragger.visible;
}
get actuallyVisible() {
return this.dragger.visible;
}
visibilityChanged() {
super.visibilityChanged();
if (!this.visibleRecursively)
this.hide();
let visible = this.actuallyVisible;
this.root.hidden = !visible;
helpers.html.setClass(
document.documentElement,
"illust-menu-visible",
visible
);
helpers.html.setClass(
this.root,
"fully-visible",
visible && !this.dragger.isAnimationPlaying
);
this.refresh();
}
show() {
this.dragger.show();
}
hide() {
this.dragger.hide();
}
toggle() {
if (this.dragger.visible)
this.hide();
else
this.show();
}
setDataSource(dataSource) {
if (this.dataSource == dataSource)
return;
this.dataSource = dataSource;
this.refresh();
}
set mediaId(mediaId) {
if (this._mediaId == mediaId)
return;
this._mediaId = mediaId;
this.refresh();
}
refresh() {
super.refresh();
if (this.dragger.isAnimationPlaying)
return;
this.getMediaInfo.id = this._mediaId;
let fullyVisible = this.dragger.position == 1;
ClassFlags.get.set("mobile-ui-visible", fullyVisible);
let visible = this.actuallyVisible;
OpenWidgets.singleton.set(this, visible);
if (!this.visible && this._mediaId != null)
return;
helpers.html.setClass(
this.root.querySelector(".button-bookmark"),
"enabled",
true
);
if (this.visible) {
let mediaId = this._mediaId;
this.bookmarkButtonWidget.setMediaId(mediaId);
this.viewMangaWidget.setMediaId(mediaId);
}
}
};
var ImageBookmarkedWidget = class extends IllustWidget {
constructor({ ...options }) {
super({
...options,
template: \`
<div class="item button-bookmark public">
<ppixiv-inline src="resources/heart-icon.svg"></ppixiv-inline>
<vv-container class="button-bookmark-icon"></vv-container>
<span class=label>Bookmark</span>
</div>
\`
});
}
get neededData() {
return "partial";
}
refreshInternal({ mediaInfo }) {
let bookmarked = mediaInfo?.bookmarkData != null;
let privateBookmark = mediaInfo?.bookmarkData?.private;
helpers.html.setClass(this.root, "enabled", mediaInfo != null);
helpers.html.setClass(this.root, "bookmarked", bookmarked);
helpers.html.setClass(this.root, "public", !privateBookmark);
}
};
var ViewMangaWidget = class extends IllustWidget {
constructor({ ...options }) {
super({
...options,
template: \`
<div class="item button-view-manga enabled">
<vv-container class=manga>
\${helpers.createIcon("ppixiv:thumbnails")}
<span class=label>Pages</span>
</vv-container>
<vv-container class=series hidden>
\${helpers.createIcon("mat:menu_book")}
<span class=label>Series</span>
</vv-container>
</div>
\`
});
this.root.addEventListener("click", (e) => this.onClick());
}
get neededData() {
return "full";
}
refreshInternal({ mediaInfo }) {
let seriesId = mediaInfo?.seriesNavData?.seriesId;
this.root.dataset.popup = mediaInfo == null ? "" : seriesId != null ? "View series" : "View manga pages";
let enabled = seriesId != null || mediaInfo?.pageCount > 1;
this.root.hidden = !enabled;
this.querySelector(".manga").hidden = seriesId != null;
this.querySelector(".series").hidden = seriesId == null;
if (seriesId != null) {
this.navigateArgs = new helpers.args("/", ppixiv.plocation);
this.navigateArgs.path = \`/user/\${mediaInfo.userId}/series/\${seriesId}\`;
} else if (mediaInfo?.pageCount > 1)
this.navigateArgs = getUrlForMediaId(mediaInfo?.mediaId, { manga: true });
else
this.navigateArgs = null;
}
onClick(e) {
if (this.navigateArgs)
helpers.navigate(this.navigateArgs);
}
};
var BookmarkTagDialog = class extends DialogWidget {
constructor({ mediaId, ...options }) {
super({
...options,
dialogClass: "mobile-tag-list",
header: "Bookmark illustration",
template: \`
<div class=menu-bar>
<vv-container class=public-bookmark></vv-container>
<vv-container class=private-bookmark></vv-container>
<vv-container class=remove-bookmark></vv-container>
</div>
\`
});
this.tagListWidget = new BookmarkTagListWidget({
container: this.root.querySelector(".scroll"),
containerPosition: "afterbegin"
});
this.publicBookmark = new BookmarkButtonWidget({
container: this.root.querySelector(".public-bookmark"),
template: \`
<div class="button-bookmark public item">
<ppixiv-inline src="resources/heart-icon.svg"></ppixiv-inline>
</div>
\`,
bookmarkType: "public",
toggleBookmark: false,
bookmarkTagListWidget: this.tagListWidget
});
this.publicBookmark.addEventListener(
"bookmarkedited",
() => this.visible = false
);
let privateBookmark = this.root.querySelector(".private-bookmark");
privateBookmark.hidden = ppixiv.native;
if (!ppixiv.native) {
this.privateBookmark = new BookmarkButtonWidget({
container: privateBookmark,
template: \`
<div class="button-bookmark-private item">
<ppixiv-inline src="resources/heart-icon-private.svg"></ppixiv-inline>
</div>
\`,
bookmarkType: "private",
toggleBookmark: false,
bookmarkTagListWidget: this.tagListWidget
});
this.privateBookmark.addEventListener(
"bookmarkedited",
() => this.visible = false
);
}
let deleteBookmark = this.root.querySelector(".remove-bookmark");
this.deleteBookmark = new BookmarkButtonWidget({
container: deleteBookmark,
template: \`
<div class="button-bookmark item icon-button">
\${helpers.createIcon("mat:delete")}
</div>
\`,
bookmarkType: "delete",
bookmarkTagListWidget: this.tagListWidget
});
this.deleteBookmark.addEventListener(
"bookmarkedited",
() => this.visible = false
);
this.tagListWidget.setMediaId(mediaId);
this.publicBookmark.setMediaId(mediaId);
this.deleteBookmark.setMediaId(mediaId);
if (this.privateBookmark)
this.privateBookmark.setMediaId(mediaId);
}
visibilityChanged() {
super.visibilityChanged();
if (this.tagListWidget)
this.tagListWidget.visible = this.actuallyVisible;
}
};
var MoreOptionsDialog = class extends DialogWidget {
constructor({ template, mediaId, ...options }) {
super({
...options,
header: "More",
classes: ["mobile-illust-ui-dialog"],
template: \`
<div class=box>
</div>
\`
});
this.moreOptionsWidget = new MoreOptionsDropdown({
container: this.root.querySelector(".box")
});
this.moreOptionsWidget.setMediaId(mediaId);
}
hide() {
this.visible = false;
}
};
var MobileIllustInfoDialog = class extends DialogWidget {
constructor({ mediaId, dataSource, ...options }) {
super({
...options,
header: "More",
classes: ["mobile-illust-ui-dialog"],
template: \`
<div class=mobile-image-info>
<div class=author-block>
<vv-container class=avatar></vv-container>
<div class=author></div>
</div>
<div class=page-count hidden></div>
<div class=image-info-text hidden></div>
<div class=post-age hidden></div>
<div class=bookmark-tags></div>
<div class=description></div>
</div>
\`
});
this.dataSource = dataSource;
this.avatarWidget = new AvatarWidget({
container: this.root.querySelector(".avatar"),
mode: "dropdown"
});
this.root.querySelector(".avatar").hidden = ppixiv.native;
this.getMediaInfo = new GetMediaInfo({
parent: this,
id: mediaId,
onrefresh: async (info) => this.refreshInternal(info)
});
}
async refreshInternal({ mediaId, mediaInfo }) {
this.root.hidden = mediaInfo == null;
if (this.root.hidden)
return;
this.querySelector(".author").textContent = \`by \${mediaInfo?.userName}\`;
this.avatarWidget.setUserId(mediaInfo?.userId);
let isLocal2 = helpers.mediaId.isLocal(mediaId);
let tags = isLocal2 ? mediaInfo.bookmarkData?.tags : mediaInfo.tagList;
tags ??= [];
let translatedTags = {};
if (!isLocal2)
translatedTags = await ppixiv.tagTranslations.getTranslations(tags, "en");
let tagWidget = this.root.querySelector(".bookmark-tags");
helpers.html.removeElements(tagWidget);
for (let tag of tags) {
let entry = this.createTemplate({
name: "tag-entry",
html: \`
<a href=# class="mobile-ui-tag-entry">
\${helpers.createIcon("ppixiv:tag", { classes: ["bookmark-tag-icon"] })}
<span class=tag-name></span>
</a>
\`
});
let translatedTag = tag;
if (translatedTags[tag])
translatedTag = translatedTags[tag];
entry.href = helpers.getArgsForTagSearch(tag, ppixiv.plocation);
entry.querySelector(".tag-name").innerText = translatedTag;
tagWidget.appendChild(entry);
}
let setInfo = (query, text) => {
let node = this.root.querySelector(query);
node.innerText = text;
node.hidden = text == "";
};
let pageCount = mediaInfo.pageCount;
let pageText = this.dataSource.getPageTextForMediaId(mediaId);
if (pageText == null && pageCount > 1) {
let currentPage = this._page;
if (currentPage > 0)
pageText = \`Page \${currentPage + 1}/\${pageCount}\`;
else
pageText = \`\${pageCount} pages\`;
}
setInfo(".page-count", pageText ?? "");
this.header = mediaInfo.illustTitle;
let info = "";
let { width, height } = ppixiv.mediaCache.getImageDimensions(
mediaInfo,
mediaId
);
if (width != null && height != null)
info += width + "x" + height;
setInfo(".image-info-text", info);
let secondsOld = (new Date() - new Date(mediaInfo.createDate)) / 1e3;
let age = helpers.strings.ageToString(secondsOld);
this.root.querySelector(".post-age").dataset.popup = helpers.strings.dateToString(mediaInfo.createDate);
setInfo(".post-age", age);
let elementComment = this.querySelector(".description");
elementComment.hidden = mediaInfo.illustComment == "";
elementComment.innerHTML = mediaInfo.illustComment;
helpers.pixiv.fixPixivLinks(elementComment);
if (!ppixiv.native)
helpers.pixiv.makePixivLinksInternal(elementComment);
}
setDataSource(dataSource) {
if (this.dataSource == dataSource)
return;
this.dataSource = dataSource;
this.refresh();
}
};
/\x2f web/vview/viewer/viewer.js
var Viewer = class extends Widget {
constructor({ mediaId, ...options }) {
super(options);
this.options = options;
this.mediaId = mediaId;
this.active = false;
this.ready = helpers.other.makePromise();
}
shutdown() {
this.ready.accept(false);
super.shutdown();
}
set active(value) {
this._active = value;
}
get active() {
return this._active;
}
toggleZoom() {
}
};
/\x2f web/vview/viewer/images/editing-overlay-container.js
var ImageEditingOverlayContainer = class extends Widget {
constructor({
...options
}) {
super({ ...options, template: \`
<div class=editing-container>
<div class=inpaint-editor-overlay-container></div>
<div class=crop-editor-overlay-container></div>
<div class=pan-editor-overlay-container></div>
</div>
\` });
this._inpaintEditorOverlayContainer = this.root.querySelector(".inpaint-editor-overlay-container");
this._cropEditorOverlayContainer = this.root.querySelector(".crop-editor-overlay-container");
this._panEditorOverlayContainer = this.root.querySelector(".pan-editor-overlay-container");
}
set inpaintEditorOverlay(node) {
helpers.html.removeElements(this._inpaintEditorOverlayContainer);
this._inpaintEditorOverlayContainer.appendChild(node);
}
set cropEditorOverlay(node) {
helpers.html.removeElements(this._cropEditorOverlayContainer);
this._cropEditorOverlayContainer.appendChild(node);
}
set panEditorOverlay(node) {
helpers.html.removeElements(this._panEditorOverlayContainer);
this._panEditorOverlayContainer.appendChild(node);
}
};
/\x2f web/vview/misc/slideshow.js
var _Slideshow = class {
constructor({
width,
height,
containerWidth,
containerHeight,
minimumZoom,
mode,
clampToWindow = true
}) {
this.width = width;
this.height = height;
this.containerWidth = containerWidth;
this.containerHeight = containerHeight;
this.minimumZoom = minimumZoom;
this.mode = mode;
this.clampToWindow = clampToWindow;
}
getDefaultAnimation() {
if (this.mode == "slideshow") {
let slideshowDefault = ppixiv.settings.get("slideshow_default");
if (slideshowDefault == "contain")
return this.getAnimation(_Slideshow.pans.stationary);
else
return this.getAnimation(_Slideshow.pans.defaultSlideshow);
}
let imageAspectRatio = this.width / this.height;
let containerAspectRatio = this.containerWidth / this.containerHeight;
let panRatio = imageAspectRatio / containerAspectRatio;
let horizontal = panRatio > 1.1;
if (containerAspectRatio < 1) {
horizontal = panRatio > 1.5;
}
let template = horizontal ? _Slideshow.pans.defaultSlideshowHoldLandscape : _Slideshow.pans.defaultSlideshowHoldPortrait;
return this.getAnimation(template);
}
getAnimation(pan) {
if (pan == null)
return this.getDefaultAnimation();
let duration = this.mode == "slideshow" || this.mode == "loop" ? ppixiv.settings.get("slideshow_duration") : ppixiv.settings.get("auto_pan_duration");
let maxSpeed = helpers.math.scaleClamp(duration, 5, 15, 0.5, 0.25);
let animationData = {
duration,
maxSpeed,
pan: [{
x: pan.x1,
y: pan.y1,
zoom: pan.start_zoom ?? 1,
anchor_x: pan.anchor?.left ?? 0.5,
anchor_y: pan.anchor?.top ?? 0.5
}, {
x: pan.x2,
y: pan.y2,
zoom: pan.end_zoom ?? 1,
anchor_x: pan.anchor?.right ?? 0.5,
anchor_y: pan.anchor?.bottom ?? 0.5
}]
};
let animation = this._prepareAnimation(animationData);
if (this.mode == "slideshow") {
animation.ease = "linear";
} else if (this.mode == "auto-pan") {
let factor = helpers.math.scaleClamp(animation.duration, 5, 15, 0.58, 1);
animation.ease = \`cubic-bezier(0.0, 0.0, \${factor}, 1.0)\`;
} else if (this.mode == "loop") {
let factor = helpers.math.scaleClamp(animation.duration, 5, 15, 0.58, 0.9);
animation.ease = \`cubic-bezier(\${1 - factor}, 0.0, \${factor}, 1.0)\`;
}
animation.fadeIn = this.mode == "loop" || this.mode == "slideshow" ? Math.min(duration * 0.1, 2.5) : 0;
animation.fadeOut = this.mode == "slideshow" ? Math.min(duration * 0.1, 2.5) : 0;
if (animation.fadeIn + animation.fadeOut > animation.duration)
animation.fadeIn = animation.fadeOut = 0;
let points = [];
for (let point of animation.pan)
points.push(\`translateX(\${point.tx}px) translateY(\${point.ty}px) scale(\${point.scale})\`);
animation.keyframes = [
{
transform: points[0],
easing: animation.ease ?? "ease-out"
},
{
transform: points[1]
}
];
return animation;
}
_prepareAnimation(animation) {
let pan = [];
for (let point of animation.pan) {
let scale2 = Math.max(point.zoom, this.minimumZoom);
let zoomedWidth = this.width * scale2;
let zoomedHeight = this.height * scale2;
let { anchor_x = 0.5, anchor_y = 0.5 } = point;
let tx = this.containerWidth * anchor_x;
let ty = this.containerHeight * anchor_y;
tx -= point.x * zoomedWidth;
ty -= point.y * zoomedHeight;
if (this.clampToWindow) {
let maxX = zoomedWidth - this.containerWidth, maxY = zoomedHeight - this.containerHeight;
tx = helpers.math.clamp(tx, 0, -maxX);
ty = helpers.math.clamp(ty, 0, -maxY);
if (zoomedWidth < this.containerWidth)
tx = (this.containerWidth - zoomedWidth) / 2;
if (zoomedHeight < this.containerHeight)
ty = (this.containerHeight - zoomedHeight) / 2;
}
pan.push({ tx, ty, zoomedWidth, zoomedHeight, scale: scale2 });
}
let corners = [];
for (let idx = 0; idx < 2; ++idx) {
corners.push([
{ x: -pan[idx].tx, y: -pan[idx].ty },
{ x: -pan[idx].tx, y: -pan[idx].ty + pan[idx].zoomedHeight },
{ x: -pan[idx].tx + pan[idx].zoomedWidth, y: -pan[idx].ty },
{ x: -pan[idx].tx + pan[idx].zoomedWidth, y: -pan[idx].ty + pan[idx].zoomedHeight }
]);
}
let distanceInPixels = 0;
for (let corner = 0; corner < 4; ++corner) {
let distance2 = helpers.math.distance(corners[0][corner], corners[1][corner]);
distanceInPixels = Math.max(distanceInPixels, distance2);
}
let screenSize = helpers.math.distance({ x: 0, y: 0 }, { x: this.containerHeight, y: this.containerWidth });
let duration = animation.duration;
if (animation.maxSpeed != null) {
let speed = Math.max(animation.maxSpeed, 0.01);
let pixelsPerSecond = speed * screenSize;
let adjustedDuration = distanceInPixels / pixelsPerSecond;
duration = Math.max(animation.duration, adjustedDuration);
if (duration == 0)
duration = 0.1;
}
return {
pan,
duration
};
}
static makeFadeIn(target, options) {
return new Animation(new KeyframeEffect(
target,
[
{ opacity: 0, offset: 0 },
{ opacity: 1, offset: 1 }
],
{
fill: "forwards",
...options
}
));
}
static makeFadeOut(target, options) {
return new Animation(new KeyframeEffect(
target,
[
{ opacity: 1, offset: 0 },
{ opacity: 0, offset: 1 }
],
{
fill: "forwards",
...options
}
));
}
};
var Slideshow = _Slideshow;
__publicField(Slideshow, "pans", {
defaultSlideshow: Object.freeze({
start_zoom: 1.25,
end_zoom: 1,
x1: 0,
y1: 1,
x2: 1,
y2: 0.1
}),
defaultSlideshowHoldPortrait: Object.freeze({
start_zoom: 1.1,
end_zoom: 1,
x1: 0.5,
y1: 0.1,
x2: 0.5,
y2: 1
}),
defaultSlideshowHoldLandscape: Object.freeze({
x1: 0,
y1: 0.5,
x2: 1,
y2: 0.5
}),
stationary: Object.freeze({
start_zoom: 0,
end_zoom: 0,
x1: 0.5,
y1: 0,
x2: 0.5,
y2: 0
})
});
/\x2f web/vview/viewer/images/editing-pan.js
var PanEditor = class extends Widget {
constructor(options) {
super({ ...options, template: \`
<div class=pan-editor>
<!-- This node is removed and placed on top of the image.-->
<div class=pan-editor-overlay>
<div class=pan-editor-crop-region>
<ppixiv-inline class="handle" src="resources/pan-editor-marker.svg"></ppixiv-inline>
<div class=monitor-preview-box><div class=box></div></div>
</div>
</div>
<div class="image-editor-buttons bottom">
<div class="image-editor-button-row box-button-row left"></div>
<div class="image-editor-button-row editor-buttons box-button-row">
\${helpers.createBoxLink({ popup: "Edit start", icon: "first_page", classes: ["edit-start-button"] })}
\${helpers.createBoxLink({ popup: "Swap start and end", icon: "swap_horiz", classes: ["swap-button"] })}
\${helpers.createBoxLink({ popup: "Edit end", icon: "last_page", classes: ["edit-end-button"] })}
\${helpers.createBoxLink({ popup: "Edit anchor", icon: "anchor", classes: ["edit-anchor"] })}
<div class="box-link popup" data-popup="Zoom">
\${helpers.createIcon("zoom_in")}
<input class=zoom-slider type=range min=5 max=200>
</div>
\${helpers.createBoxLink({ popup: "Portrait/landscape", icon: "panorama", classes: ["rotate-aspect-ratio"] })}
<div class="box-link popup aspect-ratio-slider" data-popup="Aspect ratio">
<input class=zoom-slider type=range min=0 max=3 style="width: 70px;">
</div>
\${helpers.createBoxLink({ popup: "Clear animation", icon: "delete", classes: ["reset-button"] })}
</div>
<div class="image-editor-button-row box-button-row right"></div>
</div>
</div>
\` });
this.width = this.height = 100;
this.dragging = false;
this._dragStart = null;
this.anchor = new FixedDOMRect(0.5, 0.5, 0.5, 0.5);
this._aspectRatios = [
[21, 9],
[16, 9],
[16, 10],
[4, 3]
];
this._isSet = false;
this._zoomLevel = [1, 1];
this._displayedAspectRatio = 1;
this._displayedAspectRatioPortrait = false;
this.editing = "start";
this._editingAnchor = false;
this.ui = this.root.querySelector(".editor-buttons");
this._monitorPreviewBox = this.root.querySelector(".monitor-preview-box");
this._editorOverlay = this.root.querySelector(".pan-editor-overlay");
this._editorCropRegion = this.root.querySelector(".pan-editor-crop-region");
this._editorOverlay.remove();
this._handle = this._editorOverlay.querySelector(".handle");
this._zoomSlider = this.ui.querySelector(".zoom-slider");
helpers.watchEdits(this._zoomSlider, { signal: this.shutdownSignal });
this._zoomSlider.addEventListener("editbegin", (e) => {
this.parent.saveUndo();
this._isSet = true;
});
this._zoomSlider.addEventListener("edit", (e) => {
let value = parseInt(this._zoomSlider.value) / 100;
value = 1 / value;
this._zoomLevel[this.editingIndex] = value;
this.refresh();
});
this._aspectRatioSlider = this.ui.querySelector(".aspect-ratio-slider input");
this._aspectRatioSlider.addEventListener("input", (e) => {
this._displayedAspectRatio = parseInt(this._aspectRatioSlider.value);
this.refresh();
});
this._aspectRatioSwitchButton = this.root.querySelector(".rotate-aspect-ratio");
this._aspectRatioSwitchButton.addEventListener("click", (e) => {
e.stopPropagation();
this._displayedAspectRatioPortrait = !this._displayedAspectRatioPortrait;
this.refresh();
});
this.ui.querySelector(".edit-start-button").addEventListener("click", (e) => {
e.stopPropagation();
this.editing = "start";
this.refresh();
});
this.ui.querySelector(".edit-end-button").addEventListener("click", (e) => {
e.stopPropagation();
this.editing = "end";
this.refresh();
});
this.ui.querySelector(".edit-anchor").addEventListener("click", (e) => {
e.stopPropagation();
this._editingAnchor = !this._editingAnchor;
this.refresh();
});
this.ui.querySelector(".reset-button").addEventListener("click", (e) => {
e.stopPropagation();
this.clear();
});
this.ui.querySelector(".swap-button").addEventListener("click", (e) => {
e.stopPropagation();
this.swap();
});
this.pointerListener = new PointerListener({
element: this._editorOverlay,
callback: this.pointerevent,
signal: this.shutdownSignal
});
this._editorOverlay.addEventListener("dblclick", this.ondblclick, { signal: this.shutdownSignal });
}
get editingIndex() {
return this.editing == "start" ? 0 : 1;
}
get actuallyEditingAnchor() {
return this._editingAnchor ^ this._shiftHeld;
}
set overlayContainer(overlayContainer) {
console.assert(overlayContainer instanceof ImageEditingOverlayContainer);
if (this._editorOverlay.parentNode)
this._editorOverlay.remove();
overlayContainer.panEditorOverlay = this._editorOverlay;
this._overlayContainer = overlayContainer;
}
clear() {
if (!this._isSet)
return;
this.parent.saveUndo();
this.setState(null);
}
swap() {
this.parent.saveUndo();
this._isSet = true;
this.rect = new FixedDOMRect(this.rect.x2, this.rect.y2, this.rect.x1, this.rect.y1);
this.anchor = new FixedDOMRect(this.anchor.x2, this.anchor.y2, this.anchor.x1, this.anchor.y1);
this._zoomLevel = [this._zoomLevel[1], this._zoomLevel[0]];
this.refresh();
}
get previewSize() {
let result = this._aspectRatios[this._displayedAspectRatio];
if (this._displayedAspectRatioPortrait)
return [result[1], result[0]];
else
return result;
}
refresh() {
super.refresh();
if (!this.visible)
return;
let zoom = this._zoomLevel[this.editingIndex];
this._zoomSlider.value = 1 / zoom * 100;
helpers.html.setClass(this.ui.querySelector(".edit-start-button"), "selected", this.editing == "start");
helpers.html.setClass(this.ui.querySelector(".edit-end-button"), "selected", this.editing == "end");
helpers.html.setClass(this.ui.querySelector(".edit-anchor"), "selected", this.actuallyEditingAnchor);
this._aspectRatioSwitchButton.dataset.popup = this._displayedAspectRatioPortrait ? "Previewing portrait" : "Previewing landscape";
this._aspectRatioSwitchButton.querySelector(".font-icon").innerText = this._displayedAspectRatioPortrait ? "portrait" : "panorama";
this._aspectRatioSlider.value = this._displayedAspectRatio;
this.ui.querySelector(".aspect-ratio-slider").dataset.popup = \`Previewing \${this.previewSize[0]}:\${this.previewSize[1]}\`;
this.refreshZoomPreview();
this.refreshCenter();
}
refreshCenter() {
let { x, y } = this.editing == "start" ? { x: this.rect.x1, y: this.rect.y1 } : { x: this.rect.x2, y: this.rect.y2 };
x *= this.width;
y *= this.height;
this._handle.querySelector(".crosshair").setAttribute("transform", \`translate(\${x} \${y})\`);
}
visibilityChanged() {
super.visibilityChanged();
this._editorOverlay.hidden = !this.visible;
this.ui.hidden = !this.visible;
if (this.visible) {
new KeyListener("Shift", (pressed) => {
this._shiftHeld = pressed;
this.refresh();
}, { signal: this.visibilityAbort.signal });
this.refresh();
} else {
this._shiftHeld = false;
}
}
setIllustData({ replaceEditorData, extraData, width, height }) {
this.width = width;
this.height = height;
if (extraData?.crop) {
let crop = new FixedDOMRect(extraData.crop[0], extraData.crop[1], extraData.crop[2], extraData.crop[3]);
this.width = crop.width;
this.height = crop.height;
this._editorCropRegion.style.width = \`\${100 * crop.width / width}%\`;
this._editorCropRegion.style.height = \`\${100 * crop.height / height}%\`;
this._editorCropRegion.style.top = \`\${100 * crop.top / height}%\`;
this._editorCropRegion.style.left = \`\${100 * crop.left / width}%\`;
} else {
this._editorCropRegion.style.width = this._editorCropRegion.style.height = \`\`;
this._editorCropRegion.style.top = this._editorCropRegion.style.left = \`\`;
}
this._handle.setAttribute("viewBox", \`0 0 \${this.width} \${this.height}\`);
if (replaceEditorData)
this.setState(extraData?.pan);
this.refresh();
}
getDataToSave() {
return { pan: this.getState() };
}
getState({ force = false } = {}) {
if (!force && !this._isSet)
return null;
let result = {
x1: this.rect.x1,
y1: this.rect.y1,
x2: this.rect.x2,
y2: this.rect.y2,
start_zoom: this._zoomLevel[0],
end_zoom: this._zoomLevel[1]
};
if (Math.abs(this.anchor.x1 - 0.5) > 1e-3 || Math.abs(this.anchor.y1 - 0.5) > 1e-3 || Math.abs(this.anchor.x2 - 0.5) > 1e-3 || Math.abs(this.anchor.y2 - 0.5) > 1e-3) {
result.anchor = {
left: this.anchor.x1,
top: this.anchor.y1,
right: this.anchor.x2,
bottom: this.anchor.y2
};
}
return result;
}
setState(data) {
this._isSet = data != null;
if (data == null)
data = Slideshow.pans.defaultSlideshow;
this.rect = new FixedDOMRect(data.x1, data.y1, data.x2, data.y2);
this.anchor = new FixedDOMRect(0.5, 0.5, 0.5, 0.5);
if (data.anchor)
this.anchor = new FixedDOMRect(data.anchor.left, data.anchor.top, data.anchor.right, data.anchor.bottom);
this._zoomLevel = [data.start_zoom, data.end_zoom];
this.refresh();
}
getCurrentSlideshow({ ...options } = {}) {
let [previewWidth, previewHeight] = this.previewSize;
let scaleRatio = Math.max(previewWidth / this.width, previewHeight / this.height);
let scaledWidth = this.width * scaleRatio, scaledHeight = this.height * scaleRatio;
let coverRatio = Math.min(previewWidth / scaledWidth, previewHeight / scaledHeight);
let slideshow = new Slideshow({
width: scaledWidth,
height: scaledHeight,
containerWidth: previewWidth,
containerHeight: previewHeight,
minimumZoom: coverRatio,
clampToWindow: !this.actuallyEditingAnchor,
...options
});
let panAnimation = this.getState({ force: true });
let animation = slideshow.getAnimation(panAnimation);
return { animation, scaledWidth, scaledHeight, previewWidth, previewHeight };
}
refreshZoomPreview() {
let { animation, scaledWidth, scaledHeight, previewWidth, previewHeight } = this.getCurrentSlideshow();
let pan = animation.pan[this.editingIndex];
let box = this._monitorPreviewBox.querySelector(".box");
box.style.width = \`\${100 * previewWidth / scaledWidth}%\`;
box.style.height = \`\${100 * previewHeight / scaledHeight}%\`;
let tx = 100 * -pan.tx / scaledWidth;
let ty = 100 * -pan.ty / scaledHeight;
this._monitorPreviewBox.style.width = \`\${100 / pan.scale}%\`;
this._monitorPreviewBox.style.height = \`\${100 / pan.scale}%\`;
this._monitorPreviewBox.style.transform = \`
translateX(\${tx}%)
translateY(\${ty}%)
\`;
}
pointerevent = (e) => {
if (e.pressed) {
e.preventDefault();
e.stopPropagation();
this.dragging = true;
this._dragSavedUndo = false;
this._dragPos = [e.clientX, e.clientY];
window.addEventListener("pointermove", this._pointermoveDragPoint);
return;
} else if (this.dragging != -1 && !e.pressed) {
this.dragging = false;
window.removeEventListener("pointermove", this._pointermoveDragPoint);
}
};
getPointFromClick({ clientX, clientY }) {
let { width, height, top, left } = this._editorOverlay.getBoundingClientRect();
let x = (clientX - left) / width * this.width;
let y = (clientY - top) / height * this.height;
return { x, y };
}
_pointermoveDragPoint = (e) => {
if (!this._dragSavedUndo) {
this.parent.saveUndo();
this._dragSavedUndo = true;
}
let deltaX = e.clientX - this._dragPos[0];
let deltaY = e.clientY - this._dragPos[1];
this._dragPos = [e.clientX, e.clientY];
let { width, height } = this._editorCropRegion.getBoundingClientRect();
deltaX /= width;
deltaY /= height;
let editingAnchor = this.actuallyEditingAnchor;
if (editingAnchor) {
let { animation, scaledWidth, scaledHeight, previewWidth, previewHeight } = this.getCurrentSlideshow();
let pan = animation.pan[this.editingIndex];
let monitorWidth = previewWidth / scaledWidth / pan.scale;
let monitorHeight = previewHeight / scaledHeight / pan.scale;
if (this.editing == "start") {
this.anchor.x1 += deltaX / monitorWidth;
this.anchor.y1 += deltaY / monitorHeight;
} else {
this.anchor.x2 += deltaX / monitorWidth;
this.anchor.y2 += deltaY / monitorHeight;
}
}
let rect = new FixedDOMRect(this.rect.x1, this.rect.y1, this.rect.x2, this.rect.y2);
if (this.editing == "start") {
rect.x1 += deltaX;
rect.y1 += deltaY;
} else {
rect.x2 += deltaX;
rect.y2 += deltaY;
}
this.rect = rect;
this._isSet = true;
this.refresh();
};
};
/\x2f web/vview/viewer/images/editing-inpaint.js
var InpaintEditor = class extends Widget {
constructor(options) {
super({ ...options, template: \`
<div class=inpaint-editor>
<!-- This node is removed and placed on top of the image.-->
<div class=inpaint-editor-overlay>
<svg class=inpaint-container width=100% height=100% viewBox="0 0 1 1"></svg>
</div>
<div class="image-editor-buttons bottom">
<div class="image-editor-button-row box-button-row left"></div>
<div class="image-editor-button-row editor-buttons box-button-row">
\${helpers.createBoxLink({ label: "View", classes: ["view-inpaint"] })}
\${helpers.createBoxLink({ label: "Create lines", classes: ["create-lines"] })}
<div class="inpaint-line-width-box box-link">
<span>Thickness</span>
<input class=inpaint-line-width type=range min=1 max=50>
<div class="save-default-thickness popup block-button" data-popup="Set as default">
\${helpers.createIcon("push_pin")}
</div>
</div>
<div class=box-link>
<span>Downscale</span>
<input class=inpaint-downscale type=range min=1 max=20>
<div class="save-default-downscale popup block-button" data-popup="Set as default">
\${helpers.createIcon("push_pin")}
</div>
</div>
<div class=box-link>
<span>Soften edges</span>
<input class=inpaint-blur type=range min=0 max=5>
<div class="save-default-soften popup block-button" data-popup="Set as default">
\${helpers.createIcon("push_pin")}
</div>
</div>
</div>
<div class="image-editor-button-row box-button-row right"></div>
</div>
</div>
\` });
this.width = 100;
this.height = 100;
this.lines = [];
this._downscaleRatio = 1;
this._blur = 0;
this._draggingSegmentPoint = -1;
this._dragStart = null;
this._selectedLineIdx = -1;
this.ui = this.root.querySelector(".editor-buttons");
this._editorOverlay = this.root.querySelector(".inpaint-editor-overlay");
this._editorOverlay.remove();
this._svg = this._editorOverlay.querySelector(".inpaint-container");
this._createLinesButton = this.root.querySelector(".create-lines");
this._createLinesButton.addEventListener("click", (e) => {
e.stopPropagation();
this.createLines = !this._createLines;
});
this._lineWidthSlider = this.root.querySelector(".inpaint-line-width");
this._lineWidthSliderBox = this.root.querySelector(".inpaint-line-width-box");
this._lineWidthSlider.addEventListener("input", (e) => {
if (this._selectedLine == null)
return;
this._selectedLine.thickness = parseInt(this._lineWidthSlider.value);
});
this._lineWidthSlider.value = ppixiv.settings.get("inpaint_default_thickness", 10);
new PointerListener({
element: this._lineWidthSlider,
callback: (e) => {
this._overlayContainer.hideInpaint = e.pressed;
}
});
this._downscaleSlider = this.root.querySelector(".inpaint-downscale");
this._downscaleSlider.addEventListener("change", (e) => {
this.parent.saveUndo();
this.downscaleRatio = parseFloat(this._downscaleSlider.value);
}, { signal: this.shutdownSignal });
this._blurSlider = this.root.querySelector(".inpaint-blur");
this._blurSlider.addEventListener("change", (e) => {
this.parent.saveUndo();
this.blur = parseFloat(this._blurSlider.value);
}, { signal: this.shutdownSignal });
let viewInpaintButton = this.root.querySelector(".view-inpaint");
new PointerListener({
element: viewInpaintButton,
callback: (e) => {
this.visible = !e.pressed;
},
signal: this.shutdownSignal
});
this.root.querySelector(".save-default-thickness").addEventListener("click", (e) => {
e.stopPropagation();
let value = parseInt(this._lineWidthSlider.value);
ppixiv.settings.set("inpaint_default_thickness", value);
console.log("Saved default line thickness:", value);
}, { signal: this.shutdownSignal });
this.root.querySelector(".save-default-downscale").addEventListener("click", (e) => {
e.stopPropagation();
let value = parseFloat(this._downscaleSlider.value);
ppixiv.settings.set("inpaint_default_downscale", value);
console.log("Saved default downscale:", value);
}, { signal: this.shutdownSignal });
this.root.querySelector(".save-default-soften").addEventListener("click", (e) => {
e.stopPropagation();
let value = parseFloat(this._blurSlider.value);
ppixiv.settings.set("inpaint_default_blur", value);
console.log("Saved default blur:", value);
}, { signal: this.shutdownSignal });
new PointerListener({
element: this._editorOverlay,
callback: this.pointerevent,
signal: this.shutdownSignal
});
this._ctrlPressed = false;
for (let modifier of ["Control", "Alt", "Shift"]) {
new KeyListener(modifier, (pressed) => {
this._ctrlPressed = pressed;
this._refreshPointerEvents();
}, {
signal: this.shutdownSignal
});
}
this._createLines = ppixiv.settings.get("inpaint_create_lines", false);
this._editorOverlay.addEventListener("dblclick", this.ondblclick, { signal: this.shutdownSignal });
this._editorOverlay.addEventListener("mouseover", this.onmousehover, { signal: this.shutdownSignal });
this._refreshPointerEvents();
}
shutdown() {
super.shutdown();
this.clear();
}
set overlayContainer(overlayContainer) {
console.assert(overlayContainer instanceof ImageEditingOverlayContainer);
if (this._editorOverlay.parentNode)
this._editorOverlay.remove();
overlayContainer.inpaintEditorOverlay = this._editorOverlay;
this._overlayContainer = overlayContainer;
}
refresh() {
super.refresh();
helpers.html.setClass(this._createLinesButton, "selected", this._createLines);
if (this._selectedLine)
this._lineWidthSlider.value = this._selectedLine.thickness;
this._downscaleSlider.value = this._downscaleRatio;
this._blurSlider.value = this.blur;
}
updateMenu(menuContainer) {
let create = menuContainer.querySelector(".edit-inpaint");
helpers.html.setClass(create, "enabled", true);
helpers.html.setClass(create, "selected", this.editor?._createLines);
}
visibilityChanged() {
super.visibilityChanged();
this._editorOverlay.hidden = !this.visible;
this.ui.hidden = !this.visible;
}
setIllustData({ replaceEditorData, extraData, width, height }) {
let size = Math.min(width, height);
this._lineWidthSlider.max = size / 25;
if (replaceEditorData) {
this.clear();
this.setState(extraData.inpaint);
}
if (extraData == null)
return;
this._setSize(width, height);
if (extraData.inpaint == null) {
this.downscaleRatio = ppixiv.settings.get("inpaint_default_downscale", 1);
this.blur = ppixiv.settings.get("inpaint_default_blur", 0);
}
}
getDataToSave() {
return {
inpaint: this.getState({ forSaving: true })
};
}
async afterSave(mediaInfo) {
if (mediaInfo.mangaPages[0] == null)
return;
if (mediaInfo.mangaPages[0].urls.inpaint) {
let img = new realImage();
img.src = mediaInfo.mangaPages[0].urls.inpaint;
await helpers.other.waitForImageLoad(img);
}
return true;
}
getState({ forSaving = false } = {}) {
if (forSaving && this.lines.length == 0)
return null;
let result = [];
let settings = {};
if (this._downscaleRatio != 1)
settings.downscale = this._downscaleRatio;
if (this.blur != 0)
settings.blur = this.blur;
if (Object.keys(settings).length > 0) {
settings.action = "settings";
result.push(settings);
}
for (let line of this.lines) {
let segments = [];
for (let segment of line.segments)
segments.push([segment[0], segment[1]]);
let entry = {
action: "line",
thickness: line.thickness,
line: segments
};
result.push(entry);
}
return result;
}
setState(inpaint) {
this.clear();
for (let part of inpaint || []) {
let cmd = part.action;
switch (cmd) {
case "settings":
if (part.downscale)
this.downscaleRatio = parseFloat(part.downscale);
if (part.blur)
this.blur = parseFloat(part.blur);
break;
case "line":
let line = this.addLine();
if (part.thickness)
line.thickness = part.thickness;
for (let point of part.line || [])
line.addPoint({ x: point[0], y: point[1] });
break;
default:
console.error("Unknown inpaint command:", cmd);
break;
}
}
this.refresh();
}
get downscaleRatio() {
return this._downscaleRatio;
}
set downscaleRatio(value) {
if (this._downscaleRatio == value)
return;
this._downscaleRatio = value;
this.refresh();
}
get blur() {
return this._blur;
}
set blur(value) {
if (this._blur == value)
return;
this._blur = value;
this.refresh();
}
clear() {
while (this.lines.length)
this.removeLine(this.lines[0]);
this.downscaleRatio = 1;
this._blur = 0;
}
onmousehover = (e) => {
let over = e.target.closest(".inpaint-line, .inpaint-handle") != null;
this._overlayContainer.hideInpaint = over;
if (over)
window.addEventListener("mouseover", this.onmousehover, { signal: this.shutdownSignal });
else
window.removeEventListener("mouseover", this.onmousehover, { signal: this.shutdownSignal });
};
get createLines() {
return this._createLines;
}
set createLines(value) {
if (this._createLines == value)
return;
this._createLines = value;
ppixiv.settings.set("inpaint_create_lines", this._createLines);
this._refreshPointerEvents();
if (!this._createLines && this._addingLine) {
this.removeLine(this._addingLine);
this._addingLine = null;
}
this.refresh();
}
_refreshPointerEvents() {
helpers.html.setClass(this._editorOverlay, "creating-lines", this._createLines);
if (this._ctrlPressed || this._createLines)
this._editorOverlay.style.pointerEvents = "auto";
else
this._editorOverlay.style.pointerEvents = "none";
}
_getControlPointFromElement(node) {
let inpaintSegment = node.closest(".inpaint-segment");
if (inpaintSegment)
inpaintSegment = Widget.fromNode(inpaintSegment);
let controlPoint = node.closest("[data-type='control-point']");
let inpaintLine = node.closest(".inpaint-line");
if (inpaintSegment == null)
return {};
let controlPointIdx = controlPoint ? parseInt(controlPoint.dataset.idx) : -1;
let inpaintLineIdx = inpaintLine ? parseInt(inpaintLine.dataset.idx) : -1;
if (controlPointIdx == -1 && inpaintLineIdx == -1)
inpaintSegment = null;
return { inpaintSegment, controlPointIdx, inpaintLineIdx };
}
pointerevent = (e) => {
let { x, y } = this.getPointFromClick(e);
let { inpaintSegment, controlPointIdx, inpaintLineIdx } = this._getControlPointFromElement(e.target);
this._selectedLine = inpaintSegment;
if (e.pressed && this._addingLine != null && (inpaintSegment == null || inpaintSegment != this._addingLine)) {
e.preventDefault();
e.stopPropagation();
if (inpaintSegment == this._addingLine)
return;
this.parent.saveUndo();
if (inpaintSegment && controlPointIdx != -1) {
let pointIdx = 0;
if (controlPointIdx >= inpaintSegment.segments.length / 2)
pointIdx = inpaintSegment.segments.length;
let point = this._addingLine.segments[0];
this.removeLine(this._addingLine);
this._addingLine = null;
inpaintSegment.addPoint({ x: point[0], y: point[1], at: pointIdx });
this._startDraggingPoint(inpaintSegment, controlPointIdx, e);
return;
}
let newControlPointIdx = this._addingLine.addPoint({ x, y });
this._startDraggingPoint(this._addingLine, newControlPointIdx, e);
this._addingLine = null;
return;
}
if (e.pressed && inpaintSegment) {
e.preventDefault();
e.stopPropagation();
this.parent.saveUndo();
if (controlPointIdx == -1 && e.shiftKey) {
let { x: x2, y: y2 } = this.getPointFromClick(e);
controlPointIdx = inpaintSegment.addPoint({ x: x2, y: y2, at: inpaintLineIdx });
}
this._startDraggingPoint(inpaintSegment, controlPointIdx, e);
return;
} else if (this._draggingSegment && !e.pressed) {
this._draggingSegmentPoint = -1;
window.removeEventListener("pointermove", this._pointermoveDragPoint);
}
if (e.pressed && this._createLines) {
e.preventDefault();
e.stopPropagation();
this.parent.saveUndo();
this._addingLine = this.addLine();
this._addingLine.thickness = ppixiv.settings.get("inpaint_default_thickness", 10);
let controlPointIdx2 = this._addingLine.addPoint({ x, y });
this._startDraggingPoint(this._addingLine, controlPointIdx2, e);
}
};
_startDraggingPoint(inpaintSegment, pointIdx = -1, e) {
this._draggingSegment = inpaintSegment;
this._draggingSegmentPoint = pointIdx;
this._dragPos = [e.clientX, e.clientY];
window.addEventListener("pointermove", this._pointermoveDragPoint);
}
getPointFromClick({ clientX, clientY }) {
let { width, height, top, left } = this._editorOverlay.getBoundingClientRect();
let x = (clientX - left) / width * this.width;
let y = (clientY - top) / height * this.height;
return { x, y };
}
ondblclick = (e) => {
e.stopPropagation();
let { inpaintSegment, controlPointIdx } = this._getControlPointFromElement(e.target);
if (inpaintSegment) {
this.parent.saveUndo();
if (controlPointIdx == -1)
this.removeLine(inpaintSegment);
else {
inpaintSegment.removePoint(controlPointIdx);
if (inpaintSegment.segments.length < 2)
this.removeLine(inpaintSegment);
}
}
};
_pointermoveDragPoint = (e) => {
let delta_x = e.clientX - this._dragPos[0];
let delta_y = e.clientY - this._dragPos[1];
this._dragPos = [e.clientX, e.clientY];
let { width, height } = this._editorOverlay.getBoundingClientRect();
delta_x *= this.width / width;
delta_y *= this.height / height;
let segments = this._draggingSegment.segments;
for (let idx = 0; idx < segments.length; ++idx) {
if (this._draggingSegmentPoint != -1 && this._draggingSegmentPoint != idx)
continue;
let segment = segments[idx];
segment[0] += delta_x;
segment[1] += delta_y;
segment[0] = helpers.math.clamp(segment[0], 0, this.width);
segment[1] = helpers.math.clamp(segment[1], 0, this.height);
}
this._draggingSegment.updateSegment();
};
addLine() {
let line = new LineEditorSegment({
container: this._svg
});
this.lines.push(line);
this._refreshLines();
return line;
}
removeLine(line) {
line.root.remove();
let idx = this.lines.indexOf(line);
console.assert(idx != -1);
if (this._selectedLineIdx == idx)
this._selectedLine = null;
if (this._addingLine == line)
this._addingLine = null;
this.lines.splice(idx, 1);
this._refreshLines();
}
set _selectedLine(line) {
if (line == null)
this._selectedLineIdx = -1;
else
this._selectedLineIdx = this.lines.indexOf(line);
this._refreshLines();
this.refresh();
}
get _selectedLine() {
if (this._selectedLineIdx == -1)
return null;
return this.lines[this._selectedLineIdx];
}
_refreshLines() {
for (let idx = 0; idx < this.lines.length; ++idx) {
let line = this.lines[idx];
if (idx == this._selectedLineIdx)
line.root.classList.add("selected");
else
line.root.classList.remove("selected");
}
}
_setSize(width, height) {
this.width = width;
this.height = height;
this._svg.setAttribute("viewBox", \`0 0 \${this.width} \${this.height}\`);
}
};
var LineEditorSegment = class extends Widget {
constructor({ ...options }) {
super({
...options,
template: \`
<svg>
<g class=inpaint-segment></g>
</svg>
\`
});
this._editPoints = [];
this._thickness = 15;
this.segments = [];
this.segmentLines = [];
this.segmentContainer = this.querySelector(".inpaint-segment");
this.createEditPoints();
}
get thickness() {
return this._thickness;
}
set thickness(value) {
this._thickness = value;
this.createEditPoints();
}
addPoint({ x, y, at = -1 }) {
let newSegment = [x, y];
if (at == -1)
at = this.segments.length;
this.segments.splice(at, 0, newSegment);
this.createEditPoints();
return at;
}
removePoint(idx) {
console.assert(idx < this.segments.length);
this.segments.splice(idx, 1);
this.createEditPoints();
}
createEditPoint() {
let point = document.createElementNS(helpers.other.xmlns, "ellipse");
point.setAttribute("class", "inpaint-handle");
point.setAttribute("cx", "100");
point.setAttribute("cy", "100");
point.setAttribute("rx", "10");
point.setAttribute("ry", "10");
return point;
}
createEditPoints() {
for (let line of this.segmentLines)
line.remove();
for (let point of this._editPoints)
point.remove();
this.segmentLines = [];
this._editPoints = [];
if (!this.polyline) {
this.polyline = document.createElementNS(helpers.other.xmlns, "polyline");
this.polyline.setAttribute("class", "inpaint-line");
this.segmentContainer.appendChild(this.polyline);
}
if (0)
for (let idx = 0; idx < this.segments.length - 1; ++idx) {
let line = document.createElementNS(helpers.other.xmlns, "rect");
line.setAttribute("class", "inpaint-line");
line.dataset.idx = idx;
this.segmentContainer.appendChild(line);
this.segmentLines.push(line);
}
for (let idx = 0; idx < this.segments.length; ++idx) {
let point = this.createEditPoint();
point.dataset.type = "control-point";
point.dataset.idx = idx;
this._editPoints.push(point);
this.segmentContainer.appendChild(point);
}
this.updateSegment();
}
updateSegment() {
let points = [];
for (let point of this.segments)
points.push(\`\${point[0]},\${point[1]}\`);
this.polyline.setAttribute("points", points.join(" "));
this.polyline.setAttribute("stroke-width", this._thickness);
if (0)
for (let idx = 0; idx < this.segments.length - 1; ++idx) {
let line = this.segmentLines[idx];
let p0 = this.segments[idx];
let p1 = this.segments[idx + 1];
let length = Math.pow(p0[0] - p1[0], 2) + Math.pow(p0[1] - p1[1], 2);
length = Math.sqrt(length);
let angle = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]) * 180 / Math.PI;
line.setAttribute("transform", \`translate(\${p0[0]}, \${p0[1]}) rotate(\${angle}, 0, 0) translate(0 \${-this._thickness / 2})\`);
line.setAttribute("x", 0);
line.setAttribute("y", 0);
line.setAttribute("rx", this._thickness / 4);
line.setAttribute("width", length);
line.setAttribute("height", this._thickness);
}
for (let idx = 0; idx < this.segments.length; ++idx) {
let segment = this.segments[idx];
let editPoint = this._editPoints[idx];
editPoint.setAttribute("cx", segment[0]);
editPoint.setAttribute("cy", segment[1]);
let radius = this._thickness / 2;
editPoint.setAttribute("rx", radius);
editPoint.setAttribute("ry", radius);
}
}
};
/\x2f web/vview/viewer/images/editing-crop.js
var CropEditor = class extends Widget {
constructor({ ...options }) {
super({ ...options, template: \`
<div>
<!-- This node is removed and placed on top of the image.-->
<div class="editor-overlay crop-editor-overlay">
<div class=crop-box>
<!-- Middle section for the outline on top of the others: -->
<div class=handle data-crop=all></div>
<!-- A dimmer in each direction: -->
<div class=handle data-crop=top></div>
<div class=handle data-crop=left></div>
<div class=handle data-crop=right></div>
<div class=handle data-crop=bottom></div>
<!-- Make sure the corner handles are above the edge handles. -->
<div class=handle data-crop=topleft></div>
<div class=handle data-crop=topright></div>
<div class=handle data-crop=bottomleft></div>
<div class=handle data-crop=bottomright></div>
</div>
</div>
</div>
\` });
this.width = 1;
this.height = 1;
this._editorOverlay = this.root.querySelector(".crop-editor-overlay");
this._editorOverlay.remove();
this._currentCrop = null;
this._editorOverlay.addEventListener("dblclick", this.ondblclick, { signal: this.shutdownSignal });
new PointerListener({
element: this._editorOverlay,
callback: this.pointerevent,
signal: this.shutdownSignal
});
this.box = this._editorOverlay.querySelector(".crop-box");
this.refresh();
}
ondblclick = (e) => {
e.preventDefault();
e.stopPropagation();
this.parent.saveUndo();
this._currentCrop = null;
this.refresh();
};
pointerevent = (e) => {
if (!e.pressed) {
e.preventDefault();
e.stopPropagation();
window.removeEventListener("pointermove", this.pointermove);
this._currentCrop = this._effectiveCrop;
return;
}
let clickedHandle = null;
if (this._currentCrop == null) {
let { x, y } = this.clientToContainerPos({ x: e.clientX, y: e.clientY });
this._currentCrop = new FixedDOMRect(x, y, x, y);
clickedHandle = "bottomright";
} else
clickedHandle = e.target.dataset.crop;
if (clickedHandle == null)
return;
e.preventDefault();
e.stopPropagation();
this.parent.saveUndo();
let dragParts = {
all: "move",
topleft: { y: "y1", x: "x1" },
top: { y: "y1" },
topright: { y: "y1", x: "x2" },
left: { x: "x1" },
right: { x: "x2" },
bottomleft: { y: "y2", x: "x1" },
bottom: { y: "y2" },
bottomright: { x: "x2", y: "y2" }
};
window.addEventListener("pointermove", this.pointermove);
this.dragging = dragParts[clickedHandle];
this._dragPos = this.clientToContainerPos({ x: e.clientX, y: e.clientY });
this.refresh();
};
clientToContainerPos({ x, y }) {
let { width, height, top, left } = this._editorOverlay.getBoundingClientRect();
x -= left;
y -= top;
x *= this.width / width;
y *= this.height / height;
return { x, y };
}
pointermove = (e) => {
let pos = this.clientToContainerPos({ x: e.clientX, y: e.clientY });
let delta = { x: pos.x - this._dragPos.x, y: pos.y - this._dragPos.y };
this._dragPos = pos;
if (this.dragging == "move") {
this._currentCrop.x += delta.x;
this._currentCrop.y += delta.y;
this._currentCrop.x = Math.max(0, this._currentCrop.x);
this._currentCrop.y = Math.max(0, this._currentCrop.y);
this._currentCrop.x = Math.min(this.width - this._currentCrop.width, this._currentCrop.x);
this._currentCrop.y = Math.min(this.height - this._currentCrop.height, this._currentCrop.y);
} else {
let dragging = this.dragging;
if (dragging.x != null)
this._currentCrop[dragging.x] += delta.x;
if (dragging.y != null)
this._currentCrop[dragging.y] += delta.y;
}
this.refresh();
};
get _effectiveCrop() {
if (this.dragging == null)
return this._currentCrop;
let crop = new FixedDOMRect(
this._currentCrop.x1,
this._currentCrop.y1,
this._currentCrop.x2,
this._currentCrop.y2
);
if (this.dragging != "move") {
let opposites = {
x1: "x2",
x2: "x1",
y1: "y2",
y2: "y1"
};
let minSize = 5;
if (this.dragging.x != null && Math.abs(crop.width) < minSize) {
let opposite_x = opposites[this.dragging.x];
if (crop[this.dragging.x] < crop[opposite_x])
crop[this.dragging.x] = crop[opposite_x] - minSize;
else
crop[this.dragging.x] = crop[opposite_x] + minSize;
}
if (this.dragging.y != null && Math.abs(crop.height) < minSize) {
let opposite_y = opposites[this.dragging.y];
if (crop[this.dragging.y] < crop[opposite_y])
crop[this.dragging.y] = crop[opposite_y] - minSize;
else
crop[this.dragging.y] = crop[opposite_y] + minSize;
}
}
crop = new FixedDOMRect(crop.left, crop.top, crop.right, crop.bottom);
crop = new FixedDOMRect(
Math.max(crop.left, 0),
Math.max(crop.top, 0),
Math.min(crop.right, this.width),
Math.min(crop.bottom, this.height)
);
return crop;
}
refresh() {
let box = this._editorOverlay.querySelector(".crop-box");
box.hidden = this._currentCrop == null;
if (this._currentCrop == null)
return;
let crop = this._effectiveCrop;
box.style.width = \`\${100 * crop.width / this.width}%\`;
box.style.height = \`\${100 * crop.height / this.height}%\`;
box.style.left = \`\${100 * crop.left / this.width}%\`;
box.style.top = \`\${100 * crop.top / this.height}%\`;
}
setIllustData({ replaceEditorData, extraData, width, height }) {
if (extraData == null)
return;
this.width = width;
this.height = height;
this.box.setAttribute("viewBox", \`0 0 \${this.width} \${this.height}\`);
if (replaceEditorData)
this.setState(extraData.crop);
this.refresh();
}
set overlayContainer(overlayContainer) {
console.assert(overlayContainer instanceof ImageEditingOverlayContainer);
if (this._editorOverlay.parentNode)
this._editorOverlay.remove();
overlayContainer.cropEditorOverlay = this._editorOverlay;
this._overlayContainer = overlayContainer;
}
getDataToSave() {
let state = this.getState();
return {
crop: state
};
}
async afterSave(mediaInfo) {
ppixiv.settings.set("image_editing_mode", null);
}
getState() {
if (this._currentCrop == null)
return null;
let crop = this._effectiveCrop;
return [
Math.round(crop.left),
Math.round(crop.top),
Math.round(crop.right),
Math.round(crop.bottom)
];
}
setState(crop) {
if (crop == null)
this._currentCrop = null;
else
this._currentCrop = new FixedDOMRect(crop[0], crop[1], crop[2], crop[3]);
this.refresh();
}
visibilityChanged() {
super.visibilityChanged();
this._editorOverlay.hidden = !this.visible;
}
};
/\x2f web/vview/misc/hide-mouse-cursor-on-idle.js
var _TrackMouseMovement = class {
constructor() {
_TrackMouseMovement._singleton = this;
this.forceHiddenUntil = null;
this.setMouseAnchorTimeout = -1;
this.lastMousePos = null;
window.addEventListener("mousemove", this.onmousemove, { capture: true });
}
static get singleton() {
return _TrackMouseMovement._singleton;
}
get stationary() {
return !this._active;
}
simulateInactivity() {
this.forceHiddenUntil = Date.now() + 150;
this.idle();
}
onmousemove = (e) => {
let mousePos = [e.screenX, e.screenY];
this.lastMousePos = mousePos;
if (!this.anchorPos)
this.anchorPos = this.lastMousePos;
this.clearMouseAnchorTimeout();
if (this.forceHiddenUntil && this.forceHiddenUntil > Date.now())
return;
let distanceMoved = helpers.math.distance({ x: this.anchorPos[0], y: this.anchorPos[1] }, { x: mousePos[0], y: mousePos[1] });
if (distanceMoved > 10) {
this.markMouseActive();
return;
}
this.setMouseAnchorTimeout = realSetTimeout(() => {
this.setMouseAnchorTimeout = -1;
this.anchorPos = this.lastMousePos;
}, 500);
};
clearMouseAnchorTimeout() {
if (this.setMouseAnchorTimeout == -1)
return;
realClearTimeout(this.setMouseAnchorTimeout);
this.setMouseAnchorTimeout = -1;
}
_removeTimer() {
if (!this.timer)
return;
realClearTimeout(this.timer);
this.timer = null;
}
markMouseActive() {
this.anchorPos = this.lastMousePos;
this.clearMouseAnchorTimeout();
this._removeTimer();
this.timer = realSetTimeout(this.idle, 500);
if (!this._active) {
this._active = true;
window.dispatchEvent(new Event("mouseactive"));
}
}
idle = () => {
this._removeTimer();
if (this._active) {
this._active = false;
window.dispatchEvent(new Event("mouseinactive"));
}
};
};
var TrackMouseMovement = _TrackMouseMovement;
__publicField(TrackMouseMovement, "_singleton", null);
var _HideMouseCursorOnIdle = class {
static simulateInactivity() {
TrackMouseMovement.singleton.simulateInactivity();
}
constructor(element) {
if (ppixiv.mobile)
return;
_HideMouseCursorOnIdle.addStyle();
_HideMouseCursorOnIdle.instances.add(this);
this.track = new TrackMouseMovement();
this.element = element;
window.addEventListener("mouseactive", () => this._refreshHideCursor());
window.addEventListener("mouseinactive", () => this._refreshHideCursor());
ppixiv.settings.addEventListener("no-hide-cursor", _HideMouseCursorOnIdle.updateFromSettings);
_HideMouseCursorOnIdle.updateFromSettings();
}
static addStyle() {
if (_HideMouseCursorOnIdle.globalStyle)
return;
let style = helpers.html.addStyle("hide-cursor", \`
.hide-cursor {
cursor: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="), none !important;
}
.hide-cursor * { cursor: inherit !important; }
\`);
_HideMouseCursorOnIdle.globalStyle = style;
}
static updateFromSettings() {
_HideMouseCursorOnIdle.globalStyle.disabled = !this.isEnabled;
}
static enableAll(source) {
if (ppixiv.mobile)
return;
this.disabled_by.delete(source);
this.updateFromSettings();
for (let instance of _HideMouseCursorOnIdle.instances)
instance._refreshHideCursor();
}
static disableAll(source) {
if (ppixiv.mobile)
return;
this.disabled_by.add(source);
this.updateFromSettings();
for (let instance of _HideMouseCursorOnIdle.instances)
instance._refreshHideCursor();
}
static get mouseStationary() {
return this._mouseStationary;
}
static set mouseStationary(value) {
this._mouseStationary = value;
}
static get isEnabled() {
return !ppixiv.settings.get("no-hide-cursor") && this.disabled_by.size == 0;
}
_refreshHideCursor() {
let stationary = TrackMouseMovement.singleton.stationary;
let hidden = stationary && _HideMouseCursorOnIdle.isEnabled;
helpers.html.setClass(this.element, "hide-cursor", hidden);
helpers.html.setClass(this.element, "show-cursor", !hidden);
helpers.html.setClass(this.element, "cursor-stationary", stationary);
helpers.html.setClass(this.element, "cursor-active", !stationary);
}
};
var HideMouseCursorOnIdle = _HideMouseCursorOnIdle;
__publicField(HideMouseCursorOnIdle, "instances", /* @__PURE__ */ new Set());
__publicField(HideMouseCursorOnIdle, "disabled_by", /* @__PURE__ */ new Set());
/\x2f web/vview/viewer/images/editing.js
var ImageEditor = class extends IllustWidget {
constructor({
overlayContainer,
onVisibilityChanged,
visible = null,
...options
}) {
if (visible == null)
visible = ppixiv.settings.get("image_editing", false);
super({
...options,
visible,
template: \`
<div class=image-editor>
<div class="image-editor-buttons top">
<div class="image-editor-button-row box-button-row left">
\${helpers.createBoxLink({ icon: "undo", popup: "Undo", classes: ["undo", "popup-bottom"] })}
\${helpers.createBoxLink({ icon: "redo", popup: "Redo", classes: ["redo", "popup-bottom"] })}
</div>
<div class="image-editor-button-row box-button-row center ">
\${helpers.createBoxLink({ icon: "save", popup: "Save", classes: ["save-edits", "popup-bottom"] })}
\${helpers.createBoxLink({ icon: "refresh", popup: "Saving...", classes: ["spinner"] })}
\${helpers.createBoxLink({ icon: "crop", popup: "Crop", classes: ["show-crop", "popup-bottom"] })}
\${helpers.createBoxLink({ icon: "wallpaper", popup: "Edit panning", classes: ["show-pan", "popup-bottom"] })}
\${helpers.createBoxLink({ icon: "brush", popup: "Inpainting", classes: ["show-inpaint", "popup-bottom"], dataset: { popupSide: "center" } })}
</div>
<div class="image-editor-button-row box-button-row right">
\${helpers.createBoxLink({ icon: "close", popup: "Stop editing", classes: ["close-editor", "popup-bottom"], dataset: { popupSide: "left" } })}
</div>
</div>
</div>
\`
});
this.root.querySelector(".spinner").hidden = true;
let cropEditor = new CropEditor({
container: this.root,
mode: "crop",
visible: false
});
let panEditor = new PanEditor({
container: this.root,
visible: false
});
let inpaintEditor = new InpaintEditor({
container: this.root,
visible: false
});
this.editors = {
inpaint: inpaintEditor,
crop: cropEditor,
pan: panEditor
};
this.onVisibilityChanged = onVisibilityChanged;
this._dirty = false;
this._editingMediaId = null;
this._undoStack = [];
this._redoStack = [];
this._topButtonRow = this.root.querySelector(".image-editor-buttons.top");
this._showCrop = this.root.querySelector(".show-crop");
this._showCrop.addEventListener("click", (e) => {
e.stopPropagation();
this.activeEditorName = this.activeEditorName == "crop" ? null : "crop";
});
this._showPan = this.root.querySelector(".show-pan");
this._showPan.addEventListener("click", (e) => {
e.stopPropagation();
this.activeEditorName = this.activeEditorName == "pan" ? null : "pan";
});
this._showInpaint = this.root.querySelector(".show-inpaint");
this._showInpaint.hidden = true;
this._showInpaint.addEventListener("click", (e) => {
e.stopPropagation();
this.activeEditorName = this.activeEditorName == "inpaint" ? null : "inpaint";
});
this.overlayContainer = overlayContainer;
OpenWidgets.singleton.addEventListener(
"changed",
this._refreshTemporarilyHidden,
{ signal: this.shutdownSignal }
);
window.addEventListener(
"keydown",
(e) => {
if (!this.visible)
return;
if (e.code == "KeyC" && e.ctrlKey) {
if (getSelection().toString() != "") {
console.log("Not copying editor because text is selected");
return;
}
e.preventDefault();
e.stopPropagation();
this.copy();
} else if (e.code == "KeyV" && e.ctrlKey) {
e.preventDefault();
e.stopPropagation();
this.paste();
}
},
{ signal: this.shutdownSignal }
);
for (let setting of ["image_editing", "image_editing_mode"])
ppixiv.settings.addEventListener(
setting,
() => {
this.refresh();
this.onVisibilityChanged();
},
{ signal: this.shutdownSignal }
);
this.root.addEventListener("pointerdown", (e) => {
e.stopPropagation();
});
this.root.addEventListener("dblclick", (e) => {
e.stopPropagation();
});
this._saveEdits = this.root.querySelector(".save-edits");
this._saveEdits.addEventListener(
"click",
async (e) => {
e.stopPropagation();
this.save();
},
{ signal: this.shutdownSignal }
);
this._closeEditor = this.root.querySelector(".close-editor");
this._closeEditor.addEventListener(
"click",
async (e) => {
e.stopPropagation();
ppixiv.settings.set("image_editing", null);
ppixiv.settings.set("image_editing_mode", null);
},
{ signal: this.shutdownSignal }
);
this._undoButton = this.root.querySelector(".undo");
this._redoButton = this.root.querySelector(".redo");
this._undoButton.addEventListener(
"click",
async (e) => {
e.stopPropagation();
this.undo();
},
{ signal: this.shutdownSignal }
);
this._redoButton.addEventListener(
"click",
async (e) => {
e.stopPropagation();
this.redo();
},
{ signal: this.shutdownSignal }
);
window.addEventListener(
"keydown",
(e) => {
if (e.code == "KeyS" && e.ctrlKey) {
e.stopPropagation();
e.preventDefault();
this.save();
}
if (e.code == "KeyZ" && e.ctrlKey) {
e.stopPropagation();
e.preventDefault();
this.undo();
}
if (e.code == "KeyY" && e.ctrlKey) {
e.stopPropagation();
e.preventDefault();
this.redo();
}
},
{ signal: this.shutdownSignal }
);
}
get editingCrop() {
return ppixiv.settings.get("image_editing", false) && this.activeEditorName == "crop";
}
_refreshTemporarilyHidden = () => {
let hidden = ppixiv.mobile && !OpenWidgets.singleton.empty;
helpers.html.setClass(this.root, "temporarily-hidden", hidden);
};
visibilityChanged() {
if (ppixiv.settings.get("image_editing") != this.visible)
ppixiv.settings.set("image_editing", this.visible);
this.refresh();
this.onVisibilityChanged();
super.visibilityChanged();
}
async refreshInternal({ mediaId, mediaInfo }) {
if (mediaInfo == null)
mediaId = null;
let editorIsOpen = this.openEditor != null;
let mediaIdChanging = mediaId != this._editingMediaId;
this._editingMediaId = mediaId;
let replaceEditorData = mediaIdChanging || !editorIsOpen;
let { width, height } = ppixiv.mediaCache.getImageDimensions(
mediaInfo,
mediaId
);
let extraData = ppixiv.mediaCache.getExtraData(mediaInfo, mediaId);
for (let editor of Object.values(this.editors))
editor.setIllustData({
mediaId,
extraData,
width,
height,
replaceEditorData
});
if (!editorIsOpen) {
this._undoStack = [];
this._redoStack = [];
this.dirty = false;
}
this._refreshTemporarilyHidden();
}
get openEditor() {
for (let editor of Object.values(this.editors)) {
if (editor.visible)
return editor;
}
return null;
}
set overlayContainer(overlayContainer) {
this.currentOverlayContainer = overlayContainer;
for (let editor of Object.values(this.editors))
editor.overlayContainer = overlayContainer;
}
refresh() {
super.refresh();
this.visible = ppixiv.settings.get("image_editing", false);
helpers.html.setClass(this._saveEdits, "dirty", this.dirty);
let isLocal2 = helpers.mediaId.isLocal(this._mediaId);
if (this._mediaId != null)
this._showInpaint.hidden = !isLocal2;
let showingCrop = this.activeEditorName == "crop" && this.visible;
this.editors.crop.visible = showingCrop;
helpers.html.setClass(this._showCrop, "selected", showingCrop);
let showingPan = this.activeEditorName == "pan" && this.visible;
this.editors.pan.visible = showingPan;
helpers.html.setClass(this._showPan, "selected", showingPan);
let showingInpaint = isLocal2 && this.activeEditorName == "inpaint" && this.visible;
this.editors.inpaint.visible = showingInpaint;
helpers.html.setClass(this._showInpaint, "selected", showingInpaint);
helpers.html.setClass(
this._undoButton,
"disabled",
this._undoStack.length == 0
);
helpers.html.setClass(
this._redoButton,
"disabled",
this._redoStack.length == 0
);
this._topButtonRow.querySelector(".left").hidden = this.activeEditorName == null;
if (showingCrop || showingInpaint)
HideMouseCursorOnIdle.disableAll("image-editing");
else
HideMouseCursorOnIdle.enableAll("image-editing");
}
saveUndo() {
this._undoStack.push(this.getState());
this._redoStack = [];
this.dirty = true;
}
undo() {
if (this._undoStack.length == 0)
return;
this._redoStack.push(this.getState());
this.setState(this._undoStack.pop());
this.editors.inpaint.addingline = null;
this.refresh();
}
redo() {
if (this._redoStack.length == 0)
return;
this._undoStack.push(this.getState());
this.setState(this._redoStack.pop());
this.refresh();
}
getState() {
let result = {};
for (let [name, editor] of Object.entries(this.editors))
result[name] = editor.getState();
return result;
}
setState(state) {
for (let [name, editor] of Object.entries(this.editors))
editor.setState(state[name]);
}
getDataToSave({ includeEmpty = true } = {}) {
let edits = {};
for (let editor of Object.values(this.editors)) {
for (let [key, value] of Object.entries(editor.getDataToSave())) {
if (includeEmpty || value != null)
edits[key] = value;
}
}
return edits;
}
async save() {
this.dirty = false;
let spinner = this.root.querySelector(".spinner");
this._saveEdits.hidden = true;
spinner.hidden = false;
try {
let edits = this.getDataToSave();
if (helpers.mediaId.isLocal(this._mediaId)) {
let result = await LocalAPI.localPostRequest(
\`/api/set-image-edits/\${this._mediaId}\`,
edits
);
if (!result.success) {
ppixiv.message.show(\`Error saving image edits: \${result.reason}\`);
console.error("Error saving image edits:", result);
this.dirty = true;
return;
}
ppixiv.mediaCache.addFullMediaInfo(result.illust);
} else {
await ppixiv.mediaCache.saveExtraImageData(this._mediaId, edits);
}
let currentEditor = this.activeEditor;
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(this._mediaId);
if (currentEditor?.afterSave)
currentEditor.afterSave(mediaInfo);
} finally {
this._saveEdits.hidden = false;
spinner.hidden = true;
}
}
async copy() {
let data = this.getDataToSave({ includeEmpty: false });
if (Object.keys(data).length == 0) {
ppixiv.message.show("No edits to copy");
return;
}
data.type = "ppixiv-edits";
data = JSON.stringify(data, null, 4);
await navigator.clipboard.write([
new ClipboardItem({
"text/plain": new Blob([data], { type: "text/plain" })
})
]);
ppixiv.message.show("Edits copied");
}
async paste() {
let text = await navigator.clipboard.readText();
let data;
try {
data = JSON.parse(text);
} catch (e) {
ppixiv.message.show("Clipboard doesn't contain edits");
return;
}
if (data.type != "ppixiv-edits") {
ppixiv.message.show("Clipboard doesn't contain edits");
return;
}
this.setState(data);
await this.save();
ppixiv.message.show("Edits pasted");
}
get activeEditorName() {
return ppixiv.settings.get("image_editing_mode", null);
}
set activeEditorName(editorName) {
if (editorName != null && this.editors[editorName] == null)
throw new Error(\`Invalid editor name \${editorName}\`);
ppixiv.settings.set("image_editing_mode", editorName);
}
get activeEditor() {
let currentEditor = this.activeEditorName;
if (currentEditor == null)
return null;
else
return this.editors[currentEditor];
}
get dirty() {
return this._dirty;
}
set dirty(value) {
if (this._dirty == value)
return;
this._dirty = value;
this.refresh();
}
};
/\x2f web/vview/viewer/images/viewer-images.js
var ViewerImages = class extends Viewer {
constructor({
waitForTransitions = () => {
},
...options
}) {
super({
...options,
template: \`
<div class="viewer viewer-images">
<div class=rounded-box>
<div class=rounded-box-reposition>
<div class=image-box>
<div class=crop-box></div>
</div>
</div>
</div>
</div>
\`
});
this._waitForTransitions = waitForTransitions;
this._imageBox = this.root.querySelector(".image-box");
this._cropBox = this.root.querySelector(".crop-box");
this._refreshImageRunner = new GuardedRunner(this._signal);
this._imageAspectRatio = 1;
this._actualWidth = null;
this._actualHeight = null;
this._ranPanAnimation = false;
this._centerPos = [0, 0];
this._dragMovement = [0, 0];
this._animations = {};
let enabled = ppixiv.settings.get("zoom-mode") == "locked";
let level = ppixiv.settings.get("zoom-level", "cover");
this.setZoom({ enabled, level });
this._imageContainer = new ImagesContainer({ container: this._cropBox });
this._editingContainer = new ImageEditingOverlayContainer({
container: this._cropBox
});
let resizeObserver = new ResizeObserver(this._onresize);
resizeObserver.observe(this.root);
this.shutdownSignal.addEventListener(
"abort",
() => resizeObserver.disconnect()
);
this.root.addEventListener(
"dragstart",
(e) => {
if (!e.shiftKey)
e.preventDefault();
},
this._signal
);
this.root.addEventListener(
"selectstart",
(e) => e.preventDefault(),
this._signal
);
ppixiv.settings.addEventListener(
"auto_pan",
() => {
this._ranPanAnimation = false;
this._refreshAnimation();
},
this._signal
);
ppixiv.settings.addEventListener(
"slideshow_duration",
this._refreshAnimationSpeed,
this._signal
);
ppixiv.settings.addEventListener(
"auto_pan_duration",
this._refreshAnimationSpeed,
this._signal
);
window.addEventListener(
"quickviewpointermove",
this._quickviewpointermove,
this._signal
);
OpenWidgets.singleton.addEventListener(
"changed",
() => this._checkAnimationFinished(),
this._signal
);
ppixiv.mediaCache.addEventListener(
"mediamodified",
({ mediaId }) => this._mediaInfoModified({ mediaId }),
this._signal
);
ppixiv.settings.addEventListener(
"upscaling",
() => this._refreshFromMediaInfo(),
this._signal
);
ppixiv.imageTranslations.addEventListener(
"translation-urls-changed",
() => this._refreshFromMediaInfo(),
this._signal
);
if (!ppixiv.mobile) {
this._imageEditor = new ImageEditor({
container: this.root,
parent: this,
overlayContainer: this._editingContainer,
onVisibilityChanged: () => {
this.refresh();
}
});
}
}
async load() {
let {
restoreHistory = false,
slideshow = false,
onnextimage = null
} = this.options;
this._shouldRestoreHistory = restoreHistory;
this._slideshowMode = slideshow;
this._onnextimage = onnextimage;
if (this._imageEditor)
this._imageEditor.setMediaId(this.mediaId);
this._refreshFromMediaInfo();
await ppixiv.mediaCache.getMediaInfo(this.mediaId);
if (this.shutdownSignal.aborted)
return;
this._refreshFromMediaInfo();
}
_mediaInfoModified({ mediaId }) {
if (mediaId != this.mediaId)
return;
this._refreshFromMediaInfo();
}
refresh() {
this._refreshFromMediaInfo();
}
_refreshFromMediaInfo() {
let imageInfo = this._getCurrentMediaInfo();
if (imageInfo == null)
return;
let { mediaInfo } = imageInfo;
let haveActualResolution = imageInfo.width != null;
if (this.zoomActive && this._zoomLevel == "actual" && !haveActualResolution)
return;
let { pan, crop } = ppixiv.mediaCache.getExtraData(mediaInfo, this.mediaId);
if (crop && !haveActualResolution)
return;
this._custom_animation = pan;
if (this._imageEditor?.editingCrop)
crop = null;
let croppedSize = crop && crop.length == 4 ? new FixedDOMRect(crop[0], crop[1], crop[2], crop[3]) : null;
if (imageInfo.width != null) {
this._actualWidth = imageInfo.width;
this._actualHeight = imageInfo.height;
}
this._imageAspectRatio = imageInfo.aspectRatio;
if (croppedSize)
this._imageAspectRatio = croppedSize.width / croppedSize.height;
this._updateCrop(croppedSize);
this._setImageBoxSize();
if (!this._initialPositionSet) {
this._setInitialImagePosition(this._shouldRestoreHistory);
this._initialPositionSet = true;
}
this._reposition();
let { url, previewUrl, inpaintUrl } = imageInfo;
let translationUrl = ppixiv.imageTranslations.getTranslationUrl(
this.mediaId
);
this._refreshImageRunner.call(this._refreshImage.bind(this), {
url,
previewUrl,
translationUrl,
inpaintUrl
});
}
_getCurrentMediaInfo() {
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(this.mediaId);
let page = helpers.mediaId.parse(this.mediaId).page;
if (mediaInfo != null) {
let mangaPage = mediaInfo.mangaPages[page];
let { url, width, height } = mediaInfo.getMainImageUrl(page);
return {
mediaInfo,
width,
height,
url,
previewUrl: mangaPage.urls.small,
inpaintUrl: mangaPage.urls.inpaint,
aspectRatio: width / height
};
}
mediaInfo = ppixiv.mediaCache.getMediaInfoSync(this.mediaId, {
full: false
});
if (mediaInfo == null)
return null;
let previewUrl = mediaInfo.previewUrls[page];
if (page == 0) {
let { width, height } = mediaInfo;
return {
mediaInfo,
width,
height,
previewUrl,
aspectRatio: width / height
};
}
let aspectRatio = ppixiv.extraCache.getMediaAspectRatioSync(this.mediaId);
if (aspectRatio == null)
return null;
return {
mediaInfo,
previewUrl,
aspectRatio
};
}
_updateCrop(croppedSize) {
helpers.html.setClass(this._imageBox, "cropping", croppedSize != null);
if (croppedSize == null) {
this._cropBox.style.width = "100%";
this._cropBox.style.height = "100%";
this._cropBox.style.transformOrigin = "0 0";
this._cropBox.style.transform = "";
return;
}
console.assert(this._actualWidth != null);
let cropWidth = croppedSize.width / this._actualWidth;
let cropHeight = croppedSize.height / this._actualHeight;
let cropLeft = croppedSize.left / this._actualWidth;
let cropTop = croppedSize.top / this._actualHeight;
this._cropBox.style.width = \`\${1 / cropWidth * 100}%\`;
this._cropBox.style.height = \`\${1 / cropHeight * 100}%\`;
this._cropBox.style.transformOrigin = "0 0";
this._cropBox.style.transform = \`translate(\${-cropLeft * 100}%, \${-cropTop * 100}%)\`;
}
async _refreshImage({ url, previewUrl, translationUrl, inpaintUrl, signal }) {
if (this._pointerListener)
this._pointerListener.checkMissedClicks();
if (this._slideshowMode)
previewUrl = url;
if (!LocalAPI.shouldPreloadThumbs(this.mediaId, previewUrl))
previewUrl = null;
this._imageContainer.setImageUrls({
imageUrl: url,
previewUrl,
translationUrl,
inpaintUrl
});
if (this._imageContainer.displayedImage == "main")
return;
if (!ViewerImages.decoding) {
try {
await this._imageContainer.previewImage.decode();
} catch (e) {
}
}
signal.throwIfAborted();
let imageReady = false;
let decodePromise = null;
if (!ppixiv.mobile) {
if (url != null && this._imageContainer.complete) {
decodePromise = this._decodeImage(this._imageContainer);
imageReady = await helpers.other.awaitWithTimeout(decodePromise, 50) != "timed-out";
}
signal.throwIfAborted();
}
this._imageContainer.displayedImage = imageReady ? "main" : "preview";
this.ready.accept(true);
this._refreshAnimation();
if (this._imageContainer.displayedImage == "main") {
this.pauseAnimation = false;
return;
}
if (url == null)
return;
if (this._slideshowMode)
this.pauseAnimation = true;
if (!this._imageContainer.complete) {
let result = await helpers.other.waitForImageLoad(
this._imageContainer.mainImage
);
if (result != null)
return;
signal.throwIfAborted();
}
await this._waitForTransitions();
signal.throwIfAborted();
if (!decodePromise)
decodePromise = this._decodeImage(this._imageContainer);
await decodePromise;
signal.throwIfAborted();
this.pauseAnimation = false;
this._imageContainer.displayedImage = "main";
}
async _decodeImage(img) {
ViewerImages.decoding = true;
try {
await img.decode();
} catch (e) {
} finally {
ViewerImages.decoding = false;
}
}
_removeImages() {
this._cancelSaveToHistory();
}
onkeydown = async (e) => {
if (e.ctrlKey || e.altKey || e.metaKey)
return;
switch (e.code) {
case "Home":
case "End":
e.stopPropagation();
e.preventDefault();
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(this.mediaId, {
full: false
});
if (mediaInfo == null)
return;
let newPage = e.code == "End" ? mediaInfo.pageCount - 1 : 0;
let newMediaId = helpers.mediaId.getMediaIdForPage(
this.mediaId,
newPage
);
ppixiv.app.showMediaId(newMediaId);
return;
}
};
shutdown() {
this._stopAnimation({ shuttingDown: true });
this._cancelSaveToHistory();
super.shutdown();
}
get _relativeAspect() {
let viewWidth = Math.max(this.viewWidth, 1);
let viewHeight = Math.max(this.viewHeight, 1);
let viewAspectRatio = viewWidth / viewHeight;
return viewAspectRatio > this._imageAspectRatio ? "portrait" : "landscape";
}
_setImageBoxSize() {
this._imageBox.style.width = Math.round(this.width) + "px";
this._imageBox.style.height = Math.round(this.height) + "px";
}
_onresize = (e) => {
if (this._imageContainer.displayedImage == null)
return;
this._setImageBoxSize();
this._reposition();
if (this._animationsRunning)
this._refreshAnimation();
};
getLockedZoom() {
return this._lockedZoom;
}
setZoom({ enabled = null, level = null, stopAnimation = true } = {}) {
if (stopAnimation)
this._stopAnimation();
if (enabled != null) {
if (ppixiv.mobile)
enabled = true;
this._lockedZoom = enabled;
ppixiv.settings.set("zoom-mode", enabled ? "locked" : "normal");
}
if (level != null) {
this._zoomLevel = level;
ppixiv.settings.set("zoom-level", level);
}
this._reposition();
}
getZoomLevel() {
return this._zoomLevel;
}
zoomLevelToZoomFactor(level) {
let linear = Math.pow(1.5, level);
return linear * this.containToCoverRatio;
}
zoomFactorToZoomLevel(factor) {
if (factor < 1e-5) {
console.error(\`Invalid zoom factor \${factor}\`);
factor = 1;
}
factor /= this.containToCoverRatio;
return Math.log2(factor) / Math.log2(1.5);
}
get _zoomLevelEffective() {
if (!this.zoomActive)
return 0;
else
return this._zoomLevelCurrent;
}
get _zoomFactorEffective() {
return this.zoomLevelToZoomFactor(this._zoomLevelEffective);
}
get _zoomLevelCurrent() {
let level = this._zoomLevel;
if (level == "cover")
return this._zoomLevelCover;
else if (level == "actual")
return this._zoomLevelActual;
else
return level;
}
get _zoomFactorCurrent() {
return this.zoomLevelToZoomFactor(this._zoomLevelCurrent);
}
get _zoomFactorCover() {
let result = Math.max(this.viewWidth / this.width, this.viewHeight / this.height) || 1;
return result == 0 ? 1 : result;
}
get _zoomLevelCover() {
return this.zoomFactorToZoomLevel(this._zoomFactorCover);
}
get _zoomFactorContain() {
let result = Math.min(this.viewWidth / this.width, this.viewHeight / this.height) || 1;
return result == 0 ? 1 : result;
}
get _zoomLevelContain() {
return this.zoomFactorToZoomLevel(this._zoomFactorContain);
}
get _zoomFactorActual() {
if (this._actualWidth == null)
return 1;
else
return this._actualWidth / this.width;
}
get _zoomLevelActual() {
return this.zoomFactorToZoomLevel(this._zoomFactorActual);
}
get _actualZoomAvailable() {
return this._actualWidth != null;
}
changeZoom(zoomOut, { stopAnimation = true } = {}) {
if (stopAnimation)
this._stopAnimation();
let oldLevel = this._zoomLevelEffective;
let newLevel = oldLevel;
let increment = 0.25;
if (zoomOut)
newLevel = Math.floor((newLevel - 1e-3) / increment) * increment;
else
newLevel = Math.ceil((newLevel + 1e-3) / increment) * increment;
let crossed = function(oldValue, newValue, threshold) {
return oldValue < threshold && newValue > threshold || newValue < threshold && oldValue > threshold;
};
if (crossed(oldLevel, newLevel, this._zoomLevelCover)) {
console.log("Selected cover zoom");
newLevel = "cover";
} else if (this._actualZoomAvailable && crossed(oldLevel, newLevel, this._zoomLevelActual)) {
console.log("Selected actual zoom");
newLevel = "actual";
} else {
newLevel = helpers.math.clamp(newLevel, -8, 8);
}
this.setZoom({ level: newLevel });
}
getImagePosition(viewPos, { pos = null } = {}) {
if (pos == null)
pos = this._currentZoomPos;
return [
pos[0] + (viewPos[0] - this.viewWidth / 2) / this.currentWidth,
pos[1] + (viewPos[1] - this.viewHeight / 2) / this.currentHeight
];
}
getViewPosFromImagePos(imagePos, { pos = null } = {}) {
if (pos == null)
pos = this._currentZoomPos;
return [
(imagePos[0] - pos[0]) * this.currentWidth + this.viewWidth / 2,
(imagePos[1] - pos[1]) * this.currentHeight + this.viewHeight / 2
];
}
getCenterForImagePosition(viewPos, zoomCenter) {
return [
-((viewPos[0] - this.viewWidth / 2) / this.currentWidth - zoomCenter[0]),
-((viewPos[1] - this.viewHeight / 2) / this.currentHeight - zoomCenter[1])
];
}
setImagePosition(viewPos, zoomCenter) {
this._centerPos = this.getCenterForImagePosition(viewPos, zoomCenter);
}
_quickviewpointermove = (e) => {
this.applyPointerMovement({
movementX: e.movementX,
movementY: e.movementY,
fromQuickView: true
});
};
applyPointerMovement({ movementX, movementY, fromQuickView = false } = {}) {
this._stopAnimation();
let xOffset = movementX;
let yOffset = movementY;
if (!fromQuickView) {
if (ppixiv.mobile || ppixiv.settings.get("invert-scrolling")) {
xOffset *= -1;
yOffset *= -1;
}
ppixiv.sendImage.sendMouseMovementToLinkedTabs(xOffset, yOffset);
}
xOffset /= this.currentWidth;
yOffset /= this.currentHeight;
let zoomFactor = this._zoomFactorEffective;
zoomFactor /= this.containToCoverRatio;
xOffset *= zoomFactor;
yOffset *= zoomFactor;
this._centerPos[0] += xOffset;
this._centerPos[1] += yOffset;
this._reposition();
}
get zoomActive() {
return this._mousePressed || this.getLockedZoom();
}
get _imageToCoverRatio() {
let { viewWidth, viewHeight } = this;
if (viewWidth == 0 || viewHeight == 0)
return 1;
return Math.max(viewWidth / this._imageAspectRatio, viewHeight);
}
get _imageToContainRatio() {
let { viewWidth, viewHeight } = this;
if (viewWidth == 0 || viewHeight == 0)
return 1;
return Math.min(viewWidth / this._imageAspectRatio, viewHeight);
}
get containToCoverRatio() {
return this._imageToContainRatio / this._imageToCoverRatio;
}
get width() {
return this._imageToCoverRatio * this._imageAspectRatio;
}
get height() {
return this._imageToCoverRatio;
}
get currentWidth() {
return this.width * this._zoomFactorEffective;
}
get currentHeight() {
return this.height * this._zoomFactorEffective;
}
get viewWidth() {
return this.root.offsetWidth || 1;
}
get viewHeight() {
return this.root.offsetHeight || 1;
}
get _currentZoomPos() {
if (this.zoomActive)
return [this._centerPos[0], this._centerPos[1]];
else
return [0.5, 0.5];
}
clientToViewCoords([x, y]) {
let { top, left } = this.root.getBoundingClientRect();
x -= left;
y -= top;
return [x, y];
}
viewToClientCoords([x, y]) {
let { top, left } = this.root.getBoundingClientRect();
x += left;
y += top;
return [x, y];
}
get viewPosition() {
if (this._animationsRunning)
return new FixedDOMRect(0, 0, this.viewWidth, this.viewHeight);
let viewWidth = Math.max(this.viewWidth, 1);
let viewHeight = Math.max(this.viewHeight, 1);
let { zoomPos } = this.getCurrentActualPosition();
let topLeft = this.getViewPosFromImagePos([0, 0], { pos: zoomPos });
let bottomRight = this.getViewPosFromImagePos([1, 1], { pos: zoomPos });
topLeft = [
helpers.math.clamp(topLeft[0], 0, viewWidth),
helpers.math.clamp(topLeft[1], 0, viewHeight)
];
bottomRight = [
helpers.math.clamp(bottomRight[0], 0, viewWidth),
helpers.math.clamp(bottomRight[1], 0, viewHeight)
];
return new FixedDOMRect(
topLeft[0],
topLeft[1],
bottomRight[0],
bottomRight[1]
);
}
_reposition({ clampPosition = true } = {}) {
if (this._imageContainer == null || !this._initialPositionSet)
return;
if (this.root == null || this.viewWidth == 0)
return;
this._updateRoundingBox();
if (this._animationsRunning)
return;
this._scheduleSaveToHistory();
let { zoomPos, zoomFactor, imagePosition } = this.getCurrentActualPosition({
clampPosition
});
this._centerPos = zoomPos;
this._imageBox.style.transform = \`translateX(\${imagePosition.x}px) translateY(\${imagePosition.y}px) scale(\${zoomFactor})\`;
}
_updateRoundingBox() {
let roundedBox = this.querySelector(".rounded-box");
let roundedBoxReposition = this.querySelector(".rounded-box-reposition");
if (document.documentElement.dataset.displayMode != "notch") {
roundedBox.style.translate = "";
roundedBoxReposition.style.translate = "";
roundedBox.style.width = "";
roundedBox.style.height = "";
return;
}
let { viewWidth, viewHeight } = this;
let topLeft = this.getViewPosFromImagePos([0, 0]);
topLeft[0] = Math.max(0, topLeft[0]);
topLeft[1] = Math.max(0, topLeft[1]);
let bottomRight = this.getViewPosFromImagePos([1, 1]);
bottomRight[0] = viewWidth - bottomRight[0];
bottomRight[1] = viewHeight - bottomRight[1];
bottomRight[0] = Math.max(0, bottomRight[0]);
bottomRight[1] = Math.max(0, bottomRight[1]);
if (this._animationsRunning) {
topLeft = [0, 0];
bottomRight = [0, 0];
}
roundedBox.style.translate = \`\${topLeft[0]}px \${topLeft[1]}px\`;
roundedBoxReposition.style.translate = \`\${-topLeft[0]}px \${-topLeft[1]}px\`;
let size = [
viewWidth - topLeft[0] - bottomRight[0],
viewHeight - topLeft[1] - bottomRight[1]
];
roundedBox.style.width = \`\${size[0]}px\`;
roundedBox.style.height = \`\${size[1]}px\`;
let horiz = helpers.math.scaleClamp(
size[0] / viewWidth,
0.75,
0.35,
1,
0.25
);
let vert = helpers.math.scaleClamp(
size[1] / viewHeight,
0.75,
0.35,
1,
0.25
);
roundedBox.style.setProperty("--rounding-amount", Math.min(horiz, vert));
}
getCurrentActualPosition({
zoomPos = null,
clampPosition = true
} = {}) {
let width = Math.max(this.width, 1);
let height = Math.max(this.height, 1);
let viewWidth = Math.max(this.viewWidth, 1);
let viewHeight = Math.max(this.viewHeight, 1);
let zoomFactor = this._zoomFactorEffective;
let zoomedWidth = width * zoomFactor;
let zoomedHeight = height * zoomFactor;
if (zoomPos == null)
zoomPos = this._currentZoomPos;
zoomPos = [...zoomPos];
if (clampPosition) {
if (this.zoomActive && !ppixiv.settings.get("pan-past-edge")) {
let topLeft = this.getImagePosition([0, 0], { pos: zoomPos });
topLeft[0] = Math.max(topLeft[0], 0);
topLeft[1] = Math.max(topLeft[1], 0);
zoomPos = this.getCenterForImagePosition([0, 0], topLeft);
let bottomRight = this.getImagePosition([viewWidth, viewHeight], {
pos: zoomPos
});
bottomRight[0] = Math.min(bottomRight[0], 1);
bottomRight[1] = Math.min(bottomRight[1], 1);
zoomPos = this.getCenterForImagePosition(
[viewWidth, viewHeight],
bottomRight
);
}
if (viewWidth >= Math.floor(zoomedWidth))
zoomPos[0] = 0.5;
if (viewHeight >= Math.floor(zoomedHeight))
zoomPos[1] = 0.5;
}
let x = viewWidth / 2 - zoomPos[0] * zoomedWidth;
let y = viewHeight / 2 - zoomPos[1] * zoomedHeight;
let inActualZoomMode = this._actualZoomAvailable && Math.abs(this._zoomFactorEffective - this._zoomFactorActual) < 1e-3;
if (inActualZoomMode) {
x = Math.round(x);
y = Math.round(y);
}
return { zoomPos, zoomFactor, imagePosition: { x, y } };
}
_setInitialImagePosition(restoreHistory) {
let args = helpers.args.location;
if (args.state.zoom?.animating)
this._refreshAnimation();
if (restoreHistory) {
let level = args.state.zoom?.zoom;
let enabled = args.state.zoom?.lock;
this.setZoom({ level, enabled, stopAnimation: false });
}
let aspect = this._relativeAspect;
let centerPos = [0.5, aspect == "portrait" ? 0 : 0.5];
if (args.state.zoom != null) {
let oldAspect = args.state.zoom?.relativeAspect;
let returnToTop = ppixiv.settings.get("view_mode") == "manga";
if (restoreHistory || !returnToTop && aspect == oldAspect)
centerPos = [...args.state.zoom?.pos];
}
this._centerPos = centerPos;
}
_saveToHistory = () => {
let args = helpers.args.location;
args.state.zoom = {
pos: this._centerPos,
zoom: this.getZoomLevel(),
lock: this.getLockedZoom(),
relativeAspect: this._relativeAspect,
animating: this._animationsRunning
};
helpers.navigate(args, { addToHistory: false });
};
_scheduleSaveToHistory() {
if (this._saveToHistoryId)
return;
this._saveToHistoryId = realSetTimeout(() => {
this._saveToHistoryId = null;
if (this._mousePressed) {
this._scheduleSaveToHistory();
return;
}
this._saveToHistory();
}, 250);
}
_cancelSaveToHistory() {
if (this._saveToHistoryId != null) {
realClearTimeout(this._saveToHistoryId);
this._saveToHistoryId = null;
}
}
_createCurrentAnimation() {
let animationMode;
if (this._slideshowMode == "loop")
animationMode = "loop";
else if (this._slideshowMode != null)
animationMode = "slideshow";
else if (ppixiv.settings.get("auto_pan"))
animationMode = "auto-pan";
else
return {};
if (this.root.offsetHeight == 0)
console.warn("Image container has no size");
let slideshow = new Slideshow({
width: this.width,
height: this.height,
containerWidth: this.viewWidth,
containerHeight: this.viewHeight,
mode: animationMode,
minimumZoom: this.zoomLevelToZoomFactor(0)
});
let animation = slideshow.getAnimation(this._custom_animation);
if (this.options.displayedByDrag)
animation.fadeIn = 0;
return { animationMode, animation };
}
_refreshAnimation() {
let { animationMode, animation } = this._createCurrentAnimation();
if (animation == null) {
this._stopAnimation();
return;
}
let iterationStart = 0;
if (animationMode == "loop") {
let delay = 1;
animation.duration += delay;
let fraction = delay * 0.5 / animation.duration;
animation.keyframes = [
{ ...animation.keyframes[0], offset: 0 },
{ ...animation.keyframes[0], offset: fraction },
{ ...animation.keyframes[1], offset: 1 - fraction },
{ ...animation.keyframes[1], offset: 1 }
];
}
if (this._currentAnimationMode == animationMode) {
if (ppixiv.ios)
return;
this._animations.main.effect.setKeyframes(animation.keyframes);
this._animations.main.updatePlaybackRate(1 / animation.duration);
return;
}
if (animationMode == "auto-pan") {
if (this._ranPanAnimation)
return;
this._ranPanAnimation = true;
}
this._stopAnimation();
this._currentAnimationMode = animationMode;
this._animations.main = new DirectAnimation(
new KeyframeEffect(this._imageBox, animation.keyframes, {
duration: 1e3,
fill: "forwards",
direction: animationMode == "loop" ? "alternate" : "normal",
iterations: animationMode == "loop" ? Infinity : 1,
iterationStart
})
);
this._animations.main.updatePlaybackRate(1 / animation.duration);
this._animations.main.onfinish = this._checkAnimationFinished;
if (animation.fadeIn > 0)
this._animations.fadeIn = Slideshow.makeFadeIn(this._imageBox, {
duration: animation.fadeIn * 1e3
});
if (animation.fadeOut > 0) {
this._animations.fadeOut = Slideshow.makeFadeOut(this._imageBox, {
duration: animation.fadeIn * 1e3,
delay: (animation.duration - animation.fadeOut) * 1e3
});
}
for (let animation2 of Object.values(this._animations)) {
if (animation2.playState != "finished")
animation2.play();
}
this._updateRoundingBox();
}
_checkAnimationFinished = async (e) => {
if (this._animations.main?.playState != "finished")
return;
if (this._currentAnimationMode != "slideshow" || !this._onnextimage) {
this._stopAnimation();
return;
}
if (!OpenWidgets.singleton.empty) {
console.log("Deferring next image while UI is open");
return;
}
let { mediaId } = await this._onnextimage(this);
if (mediaId == null) {
console.log(
"The slideshow didn't have a new image. Resetting the slideshow animation"
);
this._stopAnimation();
}
};
_refreshAnimationSpeed = () => {
if (!this._animationsRunning)
return;
let { animation } = this._createCurrentAnimation();
this._animations.main.updatePlaybackRate(1 / animation.duration);
};
_stopAnimation({
keepAnimations = [],
shuttingDown = false
} = {}) {
if (!this._animations.main)
return false;
let appliedAnimations = true;
try {
for (let [name, animation] of Object.entries(this._animations)) {
if (keepAnimations.indexOf(name) != -1)
continue;
animation.commitStyles();
}
} catch {
appliedAnimations = false;
}
for (let [name, animation] of Object.entries(this._animations)) {
if (keepAnimations.indexOf(name) != -1)
continue;
animation.cancel();
delete this._animations[name];
}
this._imageBox.style.opacity = "";
this._currentAnimationMode = null;
if (!appliedAnimations) {
return true;
}
let matrix = new DOMMatrix(getComputedStyle(this._imageBox).transform);
let zoomFactor = matrix.a, left = matrix.e, top = matrix.f;
let zoomLevel = this.zoomFactorToZoomLevel(zoomFactor);
if (shuttingDown)
return;
if (Math.abs(zoomLevel) < 1e-3)
this.setZoom({ enabled: false, stopAnimation: false });
else if (Math.abs(zoomLevel - this._zoomLevelCover) < 0.01)
this.setZoom({ enabled: true, level: "cover", stopAnimation: false });
else
this.setZoom({ enabled: true, level: zoomLevel, stopAnimation: false });
this.setImagePosition([left, top], [0, 0]);
this._reposition();
return true;
}
get _animationsRunning() {
return this._animations.main != null;
}
set pauseAnimation(pause) {
this._pauseAnimation = pause;
this.refreshAnimationPaused();
}
refreshAnimationPaused() {
for (let animation of Object.values(this._animations)) {
if (this._pauseAnimation && animation.playState == "running")
animation.pause();
else if (!this._pauseAnimation && animation.playState == "paused")
animation.play();
}
}
zoomToggle({ x, y } = {}) {
if (this._slideshowMode)
return;
this._stopAnimation();
if (x == null || y == null) {
x = this.viewWidth / 2;
y = this.viewHeight / 2;
}
let center = this.getImagePosition([x, y]);
this.setZoom({ enabled: !this.getLockedZoom() });
this.setImagePosition([x, y], center);
this._reposition();
}
zoomSetLevel(level, { x, y }) {
if (this._slideshowMode)
return;
if (level == "actual" && this._actualWidth == null) {
console.log("Can't display actual zoom yet");
return;
}
this._stopAnimation();
if (this.getZoomLevel() == level && this.getLockedZoom()) {
this.setZoom({ enabled: false });
this._reposition();
return;
}
let center = this.getImagePosition([x, y]);
this.setZoom({ enabled: true, level });
this.setImagePosition([x, y], center);
this._reposition();
}
zoomAdjust(down, { x, y }) {
if (this._slideshowMode)
return;
this._stopAnimation();
if (x == null || y == null) {
x = this.viewWidth / 2;
y = this.viewHeight / 2;
}
let center = this.getImagePosition([x, y]);
if (!this.zoomActive)
this.setZoom({ enabled: true, level: 0 });
let oldZoomLevel = this._zoomLevelEffective;
this.changeZoom(down);
let newZoomLevel = this._zoomLevelEffective;
if (Math.abs(oldZoomLevel - newZoomLevel) < 0.01)
this.changeZoom(down);
if (this.getZoomLevel() == 0)
this.setZoom({ enabled: false, level: "cover" });
this.setImagePosition([x, y], center);
this._reposition();
}
};
var ImagesContainer = class extends Widget {
constructor({ ...options }) {
super({
...options,
template: \`
<div class=inner-image-container>
<img class="filtering displayed-image main-image" hidden>
<img class="filtering displayed-image translation-image" hidden>
<img class="filtering displayed-image inpaint-image" hidden>
<img class="filtering displayed-image low-res-preview" hidden>
</div>
\`
});
this.mainImage = this.root.querySelector(".main-image");
this.inpaintImage = this.root.querySelector(".inpaint-image");
this.translationImage = this.root.querySelector(".translation-image");
this.previewImage = this.root.querySelector(".low-res-preview");
this.mainImage.fetchPriority = "high";
this.inpaintImage.fetchPriority = "high";
this.translationImage.fetchPriority = "high";
this.previewImage.fetchPriority = "high";
}
shutdown() {
if (this.mainImage) {
this.mainImage.src = helpers.other.blankImage;
this.mainImage.remove();
this.mainImage = null;
}
if (this.previewImage) {
this.previewImage.src = helpers.other.blankImage;
this.previewImage.remove();
this.previewImage = null;
}
super.shutdown();
}
setImageUrls({ imageUrl, inpaintUrl, translationUrl, previewUrl }) {
function setImageSource(img, src) {
if (src)
img.src = src;
else
img.removeAttribute("src");
}
setImageSource(this.mainImage, imageUrl);
setImageSource(this.inpaintImage, inpaintUrl);
setImageSource(this.translationImage, translationUrl);
setImageSource(this.previewImage, previewUrl);
this._refreshInpaintVisibility();
}
get complete() {
return this.mainImage.complete && this.inpaintImage.complete && this.translationImage.complete;
}
decode() {
let promises = [];
if (this.mainImage.src)
promises.push(this.mainImage.decode());
if (this.inpaintImage.src)
promises.push(this.inpaintImage.decode());
if (this.translationImage.src)
promises.push(this.translationImage.decode());
return Promise.all(promises);
}
set displayedImage(displayedImage) {
this.mainImage.hidden = displayedImage != "main";
this.previewImage.hidden = displayedImage != "preview";
this._refreshInpaintVisibility();
}
get displayedImage() {
if (!this.mainImage.hidden)
return "main";
else if (!this.previewImage.hidden)
return "preview";
else
return null;
}
_refreshInpaintVisibility() {
this.inpaintImage.hidden = this.mainImage.hidden || !this.inpaintImage.src;
this.translationImage.hidden = this.mainImage.hidden || !this.translationImage.src;
}
get width() {
return this.mainImage.width;
}
get height() {
return this.mainImage.height;
}
get naturalWidth() {
return this.mainImage.naturalWidth;
}
get naturalHeight() {
return this.mainImage.naturalHeight;
}
get hideInpaint() {
return this.inpaintImage.style.opacity == 0;
}
set hideInpaint(value) {
this.inpaintImage.style.opacity = value ? 0 : 1;
}
};
/\x2f web/vview/viewer/images/desktop-viewer-images.js
var ViewerImagesDesktop = class extends ViewerImages {
constructor({ ...options }) {
super(options);
window.addEventListener("blur", (e) => this.stopDragging(), this._signal);
this._pointerListener = new PointerListener({
element: this.root,
buttonMask: 1,
signal: this.shutdownSignal,
callback: this._pointerevent
});
}
_pointerevent = (e) => {
if (e.mouseButton != 0 || this._slideshowMode)
return;
if (e.shiftKey)
return;
if (e.pressed && this.capturedPointerId == null) {
e.preventDefault();
this.root.style.cursor = "none";
ClassFlags.get.set("hide-ui", true);
if (e.type != "simulatedpointerdown")
this._stopAnimation();
let zoomCenterPos;
if (!this.getLockedZoom())
zoomCenterPos = this.getImagePosition([e.clientX, e.clientY]);
let allowZoom = true;
if (e.type == "simulatedpointerdown" && !this.getLockedZoom())
allowZoom = false;
if (allowZoom)
this._mousePressed = true;
this._dragMovement = [0, 0];
this.capturedPointerId = e.pointerId;
this.root.setPointerCapture(this.capturedPointerId);
this.root.addEventListener("lostpointercapture", this._lostPointerCapture, this._signal);
if (!this.getLockedZoom())
this.setImagePosition([e.clientX, e.clientY], zoomCenterPos);
this._reposition();
this.root.addEventListener("pointermove", this._pointermove, this._signal);
} else {
if (this.capturedPointerId == null || e.pointerId != this.capturedPointerId)
return;
HideMouseCursorOnIdle.simulateInactivity();
this.stopDragging();
}
};
shutdown() {
ClassFlags.get.set("hide-ui", false);
super.shutdown();
}
stopDragging() {
this._saveToHistory();
if (this.root != null) {
this.root.removeEventListener("pointermove", this._pointermove);
this.root.style.cursor = "";
}
if (this.capturedPointerId != null) {
this.root.releasePointerCapture(this.capturedPointerId);
this.capturedPointerId = null;
}
this.root.removeEventListener("lostpointercapture", this._lostPointerCapture);
ClassFlags.get.set("hide-ui", false);
this._mousePressed = false;
this._reposition();
}
_lostPointerCapture = (e) => {
if (e.pointerId == this.capturedPointerId)
this.capturedPointerId = null;
};
_pointermove = (e) => {
if (e.movementX == 0 && e.movementY == 0)
return;
let { movementX, movementY } = e;
if (devicePixelRatio != null) {
movementX /= devicePixelRatio;
movementY /= devicePixelRatio;
}
this._dragMovement[0] += movementX;
this._dragMovement[1] += movementY;
if (this._animationsRunning && this._dragMovement[0] < 4 && this._dragMovement[1] < 4)
return;
this.applyPointerMovement({ movementX, movementY });
};
};
/\x2f web/vview/viewer/images/mobile-touch-scroller.js
var FlingFriction = 7;
var FlingMinimumVelocity = 10;
var TouchScroller = class extends Actor {
constructor({
container,
setPosition,
getPosition,
adjustZoom,
getBounds,
getWantedZoom,
onactive = () => {
},
oninactive = () => {
},
ondragstart = () => {
},
ondragend = () => {
},
onanimationstart = () => {
},
onanimationfinished = () => {
},
...options
}) {
super(options);
this.root = container;
this.options = {
getPosition,
setPosition,
getBounds,
getWantedZoom,
adjustZoom,
onactive,
oninactive,
ondragstart,
ondragend,
onanimationstart,
onanimationfinished
};
this.velocity = { x: 0, y: 0 };
this._flingVelocity = new FlingVelocity();
this._state = "idle";
this.shutdownSignal.addEventListener("abort", (e) => this.cancelFling(), { once: true });
this.dragger = new DragHandler({
parent: this,
name: "TouchScroller",
element: container,
pinch: true,
deferDelayMs: 30,
confirmDrag: ({ event }) => !helpers.shouldIgnoreHorizontalDrag(event),
ondragstart: (...args) => this._ondragstart(...args),
ondrag: (...args) => this._ondrag(...args),
ondragend: (...args) => this._ondragend(...args)
});
}
get state() {
return this._state;
}
cancelDrag() {
if (this._state != "dragging")
return;
this.dragger.cancelDrag();
this._setState("idle");
}
_setState(state, args = {}) {
if (state == this._state)
return;
if (state != "idle" && this._changeState("idle", "active"))
this.options.onactive(args);
if (state != "dragging" && this._changeState("dragging", "active"))
this.options.ondragend(args);
if (state != "animating" && this._changeState("animating", "active"))
this.options.onanimationfinished(args);
if (state == "dragging" && this._changeState("active", "dragging"))
this.options.ondragstart(args);
if (state == "animating" && this._changeState("active", "animating"))
this.options.onanimationstart(args);
if (state == "idle" && this._changeState("active", "idle"))
this.options.oninactive(args);
}
_changeState(oldState, newState) {
if (this._state != oldState)
return false;
this._state = newState;
return true;
}
_ondragstart() {
if (this._state == "animating")
this.cancelFling();
this._setState("dragging");
this._flingVelocity.reset();
let { width, height } = this.options.getBounds();
this.dragAxesLocked = [width < 1e-3, height < 1e-3];
return true;
}
_ondrag({
first,
movementX,
movementY,
x,
y,
radius,
previousRadius
}) {
if (this._state != "dragging")
return;
if (first)
return;
let position = this.options.getPosition();
let bounds = this.options.getBounds();
let overscrollX = Math.max(bounds.left - position.x, position.x - bounds.right);
let overscrollY = Math.max(bounds.top - position.y, position.y - bounds.bottom);
if (overscrollX > 0)
movementX *= Math.pow(this.overscrollStrength, overscrollX);
if (overscrollY > 0)
movementY *= Math.pow(this.overscrollStrength, overscrollY);
if (this.dragAxesLocked[0])
movementX = 0;
if (this.dragAxesLocked[1])
movementY = 0;
this.options.setPosition({ x: position.x - movementX, y: position.y - movementY });
this._flingVelocity.addSample({ x: -movementX, y: -movementY });
if (bounds.width >= 1e-3)
this.dragAxesLocked[0] = false;
if (bounds.height >= 1e-3)
this.dragAxesLocked[1] = false;
if (previousRadius > 0) {
let ratio = radius / previousRadius;
this.options.adjustZoom({ ratio, centerX: x, centerY: y });
}
}
_ondragend(e) {
if (this._state == "dragging")
this.startFling();
}
get overscrollStrength() {
return 0.994;
}
startFling({ onanimationstartOptions = {} } = {}) {
if (this._state == "animating") {
console.warn("Already animating");
return;
}
if (this.dragger.isDragging) {
return;
}
this.velocity = this._flingVelocity.currentVelocity;
this._setState("animating", onanimationstartOptions);
console.assert(this._abortFling == null);
this._abortFling = new AbortController();
this._runFling(this._abortFling.signal);
}
async _runFling(signal) {
let previousTime = Date.now() / 1e3;
while (this._state == "animating") {
let success = await helpers.other.vsync({ signal });
if (!success)
return;
let newTime = Date.now() / 1e3;
let duration = newTime - previousTime;
previousTime = newTime;
let movementX = this.velocity.x * duration;
let movementY = this.velocity.y * duration;
let currentPosition = this.options.getPosition();
currentPosition.x += movementX;
currentPosition.y += movementY;
let decay = Math.exp(-FlingFriction * duration);
this.velocity.x *= decay;
this.velocity.y *= decay;
let bounced = this.applyPositionBounce(duration, currentPosition);
if (this._applyZoomBounce(duration))
bounced = true;
let totalVelocity = Math.pow(Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.y, 2), 0.5);
if (!bounced && totalVelocity < FlingMinimumVelocity)
break;
}
this.velocity = { x: 0, y: 0 };
this._abortFling = null;
this._setState("idle");
}
_applyZoomBounce(duration) {
let { ratio, centerX, centerY } = this.options.getWantedZoom();
if (Math.abs(1 - ratio) < 1e-3)
return false;
let inverted = ratio < 1;
if (inverted)
ratio = 1 / ratio;
let zoomRatioPerSecond = Math.pow(ratio, 10);
let maxRatioThisFrame = Math.pow(zoomRatioPerSecond, duration);
ratio = Math.min(ratio, maxRatioThisFrame);
if (inverted)
ratio = 1 / ratio;
this.options.adjustZoom({ ratio, centerX, centerY });
return true;
}
applyPositionBounce(duration, position) {
let bounds = this.options.getBounds();
let factor = 0.025;
if (position.x < bounds.left) {
let bounceVelocity = bounds.left - position.x;
bounceVelocity *= factor;
position.x += bounceVelocity * duration * 300;
if (position.x >= bounds.left - 1)
position.x = bounds.left;
}
if (position.x > bounds.right) {
let bounceVelocity = bounds.right - position.x;
bounceVelocity *= factor;
position.x += bounceVelocity * duration * 300;
if (position.x <= bounds.right + 1)
position.x = bounds.right;
}
if (position.y < bounds.top) {
let bounceVelocity = bounds.top - position.y;
bounceVelocity *= factor;
position.y += bounceVelocity * duration * 300;
if (position.y >= bounds.top - 1)
position.y = bounds.top;
}
if (position.y > bounds.bottom) {
let bounceVelocity = bounds.bottom - position.y;
bounceVelocity *= factor;
position.y += bounceVelocity * duration * 300;
if (position.y <= bounds.bottom + 1)
position.y = bounds.bottom;
}
this.options.setPosition(position);
return position.x < bounds.left || position.y < bounds.top || position.x > bounds.right || position.y > bounds.bottom;
}
cancelFling() {
if (this._state != "animating")
return;
if (this._abortFling) {
this._abortFling.abort();
this._abortFling = null;
}
this._setState("idle");
}
};
/\x2f web/vview/viewer/images/mobile-viewer-images.js
var ViewerImagesMobile = class extends ViewerImages {
constructor({ ...options }) {
super(options);
this.root.addEventListener("pointerdown", (e) => {
if (this._slideshowMode || !this._animationsRunning)
return;
e.partiallyHandled = true;
});
this._touchScroller = new TouchScroller({
parent: this,
container: this.root,
onactive: () => {
if (!this._slideshowMode)
this._stopAnimation();
},
getPosition: () => {
if (!this._slideshowMode)
this._stopAnimation();
let x = this._centerPos[0] * this.currentWidth;
let y = this._centerPos[1] * this.currentHeight;
[x, y] = this.viewToClientCoords([x, y]);
return { x, y };
},
setPosition: ({ x, y }) => {
if (this._slideshowMode)
return;
this._stopAnimation();
[x, y] = this.clientToViewCoords([x, y]);
x /= this.currentWidth;
y /= this.currentHeight;
this._centerPos[0] = x;
this._centerPos[1] = y;
this._reposition();
},
adjustZoom: ({ ratio, centerX, centerY }) => {
if (this._slideshowMode)
return;
this._stopAnimation();
let [viewX, viewY] = this.clientToViewCoords([centerX, centerY]);
let center = this.getImagePosition([viewX, viewY]);
let newFactor = this._zoomFactorCurrent * ratio;
let newLevel = this.zoomFactorToZoomLevel(newFactor);
if (Math.abs(newLevel) < 5e-3)
newLevel = 0;
this._zoomLevel = newLevel;
this.setImagePosition([viewX, viewY], center);
this._reposition();
},
getBounds: () => {
let topLeft = this.getCurrentActualPosition({ zoomPos: [0, 0] }).zoomPos;
let bottomRight = this.getCurrentActualPosition({ zoomPos: [1, 1] }).zoomPos;
if (this.moveToTarget) {
topLeft = this.getCurrentActualPosition({ zoomPos: this.targetZoomCenter }).zoomPos;
bottomRight = [...topLeft];
}
topLeft[0] *= this.currentWidth;
topLeft[1] *= this.currentHeight;
bottomRight[0] *= this.currentWidth;
bottomRight[1] *= this.currentHeight;
topLeft = this.viewToClientCoords(topLeft);
bottomRight = this.viewToClientCoords(bottomRight);
return new FixedDOMRect(topLeft[0], topLeft[1], bottomRight[0], bottomRight[1]);
},
onanimationstart: ({ touchFactor = null, targetImagePos = null, moveToTarget = false } = {}) => {
this.moveToTarget = moveToTarget;
if (touchFactor != null) {
this.targetZoomFactor = touchFactor;
this.targetZoomCenter = targetImagePos;
return;
}
this.targetZoomCenter = [0.5, 0.5];
let zoomFactorCover = this._zoomFactorCover;
let zoomFactorCurrent = this._zoomFactorCurrent;
if (this._zoomFactorCurrent < this._zoomFactorContain + 0.01)
this.targetZoomFactor = this._zoomFactorContain;
else if (Math.abs(zoomFactorCover - zoomFactorCurrent) < 0.15)
this.targetZoomFactor = this._zoomFactorCover;
else
this.targetZoomFactor = this._zoomFactorCurrent;
},
onanimationfinished: () => {
this.moveToTarget = false;
ppixiv.settings.set("zoom-level", this._zoomLevel == 0 ? 0 : "cover");
},
getWantedZoom: () => {
let [viewX, viewY] = this.getViewPosFromImagePos(this.targetZoomCenter);
let [centerX, centerY] = this.viewToClientCoords([viewX, viewY]);
return {
ratio: this.targetZoomFactor / this._zoomFactorCurrent,
centerX,
centerY
};
}
});
}
toggleZoom(e) {
if (this._slideshowMode)
return;
this._stopAnimation();
this._touchScroller.cancelFling();
let zoomInLevel;
let zoomOutLevel = 0;
let coverZoomRatio = 1 / this.zoomLevelToZoomFactor(0);
if (coverZoomRatio > 1.5)
zoomInLevel = this._zoomLevelCover;
else {
let scaledZoomFactor = this._zoomFactorCover * 2;
let scaledZoomLevel = this.zoomFactorToZoomLevel(scaledZoomFactor);
zoomInLevel = scaledZoomLevel;
}
let currentZoomLevel = this._zoomLevelCurrent;
let zoomDistanceIn = Math.abs(currentZoomLevel - zoomInLevel);
let zoomDistanceOut = Math.abs(currentZoomLevel - zoomOutLevel);
let level = zoomDistanceIn > zoomDistanceOut ? zoomInLevel : zoomOutLevel;
let touchFactor = this.zoomLevelToZoomFactor(level);
let viewPos = this.clientToViewCoords([e.clientX, e.clientY]);
let targetImagePos = this.getImagePosition(viewPos);
this._touchScroller.startFling({
onanimationstartOptions: {
touchFactor,
targetImagePos,
moveToTarget: true
}
});
}
_reposition({ clampPosition = true, ...options } = {}) {
if (this._touchScroller) {
if (this._touchScroller.state != "idle")
clampPosition = false;
}
return super._reposition({ clampPosition, ...options });
}
};
/\x2f web/vview/viewer/video/seek-bar.js
var SeekBar = class extends Widget {
constructor({ ...options }) {
super({
...options,
template: \`
<div class="seek-bar">
<div class=seek-parts>
<div data-seek-part=empty class=seek-empty></div>
<div data-seek-part=loaded class=seek-loaded></div>
<div data-seek-part=fill class=seek-fill></div>
</div>
</div>
\`
});
this.currentTime = 0;
this.duration = 1;
this.amountLoaded = 1;
this.refresh();
this.setCallback(null);
this.dragger = new DragHandler({
element: this.root,
signal: this.shutdownSignal,
name: "seek-bar",
deferredStart: () => false,
confirmDrag: () => {
return this.callback != null;
},
ondragstart: ({ event }) => {
helpers.html.setClass(this.root, "dragging", true);
this.setDragPos(event);
return true;
},
ondrag: ({ event, first }) => {
this.setDragPos(event);
},
ondragend: () => {
helpers.html.setClass(this.root, "dragging", false);
if (this.callback)
this.callback(false, null);
}
});
}
setDragPos(e) {
let bounds = this.root.getBoundingClientRect();
let pos = (e.clientX - bounds.left) / bounds.width;
pos = Math.max(0, Math.min(1, pos));
let time = pos * this.duration;
this.callback(true, time);
}
setCallback(callback) {
if (this.callback == callback)
return;
if (this.callback != null)
this.dragger.cancelDrag();
this.callback = callback;
}
setDuration(seconds) {
this.duration = seconds;
this.refresh();
}
setCurrentTime(seconds) {
this.currentTime = seconds;
this.refresh();
}
setLoaded(value) {
this.amountLoaded = value;
this.refresh();
}
refresh() {
let position = this.duration > 1e-4 ? this.currentTime / this.duration : 0;
this.root.querySelector(".seek-fill").style.width = position * 100 + "%";
let loaded = this.amountLoaded < 1 ? this.amountLoaded : 0;
this.root.querySelector(".seek-loaded").style.width = loaded * 100 + "%";
}
};
/\x2f web/vview/viewer/video/video-ui.js
var VideoUI = class extends Widget {
constructor({ ...options }) {
super({
...options,
template: \`
<div class=video-ui>
<div class=seek-bar-container-top></div>
<div class=video-ui-strip>
<vv-container class="play-button button">
\${helpers.createIcon("pause", { dataset: { play: "pause" } })}
\${helpers.createIcon("play_arrow", { dataset: { play: "play" } })}
</vv-container>
<div class=time></div>
<div style="flex: 1;"></div>
<vv-container class=volume-slider-container data-hidden-on=mobile></vv-container>
<vv-container class=button>
\${helpers.createIcon("volume_up", { dataset: { volume: "high" } })}
\${helpers.createIcon("volume_off", { dataset: { volume: "mute" } })}
</vv-container>
<vv-container class="pip-button button">
\${helpers.createIcon("picture_in_picture_alt")}
</vv-container>
<vv-container class="fullscreen button">
<ppixiv-inline src="resources/fullscreen.svg"></ppixiv-inline>
</vv-container>
</div>
<div class=seek-bar-container-bottom></div>
</div>
\`
});
this.refreshShowUi();
ClassFlags.get.addEventListener("mobile-ui-visible", (e) => {
this.refreshShowUi();
}, { signal: this.shutdownSignal });
new PointerListener({
element: this.root,
callback: (e) => {
helpers.html.setClass(this.root, "dragging", e.pressed);
}
});
this.seekBar = new SeekBar({
container: this.root.querySelector(".seek-bar-container-top")
});
this._setSeekBarPos();
this.volumeSlider = new VolumeSliderWidget({
container: this.root.querySelector(".volume-slider-container"),
startedDragging: () => {
this.savedVolume = this.video.volume;
},
stoppedDragging: () => {
this.savedVolume = null;
},
ondrag: (volume) => {
if (!this.video)
return;
if (volume == 0) {
this.video.volume = this.savedVolume;
this.video.muted = true;
} else {
this.video.volume = volume;
this.video.muted = false;
}
}
});
this.time = this.root.querySelector(".time");
this.root.addEventListener("dblclick", (e) => {
e.stopPropagation();
e.preventDefault();
});
this.root.querySelector(".play-button").addEventListener("click", () => {
if (this.player != null)
this.player.setWantPlaying(!this.player.wantPlaying);
}, { signal: this.shutdownSignal });
for (let button of this.root.querySelectorAll("[data-volume]"))
button.addEventListener("click", () => {
if (this.video == null)
return;
this.video.muted = !this.video.muted;
}, { signal: this.shutdownSignal });
this.root.querySelector(".pip-button").addEventListener("click", async () => {
if (this.video == null)
return;
if (this.video.requestPictureInPicture == null)
return false;
try {
await this.video.requestPictureInPicture();
return true;
} catch (e) {
return false;
}
}, { signal: this.shutdownSignal });
document.addEventListener("fullscreenchange", (e) => {
this._setSeekBarPos();
}, { signal: this.shutdownSignal });
window.addEventListener("resize", (e) => {
this._setSeekBarPos();
}, { signal: this.shutdownSignal });
let fullscreenButton = this.root.querySelector(".fullscreen");
fullscreenButton.hidden = ppixiv.mobile;
fullscreenButton.addEventListener("click", () => {
helpers.toggleFullscreen();
}, { signal: this.shutdownSignal });
this.videoChanged();
}
refreshShowUi() {
let show_ui = ClassFlags.get.get("mobile-ui-visible");
helpers.html.setClass(this.root, "show-ui", show_ui);
}
_setSeekBarPos() {
let top = ppixiv.mobile || !helpers.isFullscreen();
this.seekBar.root.remove();
let seekBarContainer = top ? ".seek-bar-container-top" : ".seek-bar-container-bottom";
this.root.querySelector(seekBarContainer).appendChild(this.seekBar.root);
this.seekBar.root.dataset.position = top ? "top" : "bottom";
}
shutdown() {
this.videoChanged();
super.shutdown();
}
videoChanged({ player = null, video = null } = {}) {
if (this.removeVideoListeners) {
this.removeVideoListeners.abort();
this.removeVideoListeners = null;
}
this.player = player;
this.video = video;
this.root.querySelector(".video-ui-strip").hidden = this.video == null;
if (this.video == null)
return;
this.removeVideoListeners = new AbortController();
this.video.addEventListener("volumechange", (e) => {
this.volumeChanged();
}, { signal: this.removeVideoListeners.signal });
this.video.addEventListener("play", (e) => {
this.pauseChanged();
}, { signal: this.removeVideoListeners.signal });
this.video.addEventListener("pause", (e) => {
this.pauseChanged();
}, { signal: this.removeVideoListeners.signal });
this.video.addEventListener("timeupdate", (e) => {
this.timeChanged();
}, { signal: this.removeVideoListeners.signal });
this.video.addEventListener("loadedmetadata", (e) => {
this.timeChanged();
}, { signal: this.removeVideoListeners.signal });
this.video.addEventListener("progress", (e) => {
this.timeChanged();
}, { signal: this.removeVideoListeners.signal });
this.root.querySelector(".pip-button").hidden = this.video.requestPictureInPicture == null;
this.pauseChanged();
this.volumeChanged();
this.timeChanged();
}
pauseChanged() {
this.root.querySelector("[data-play='play']").style.display = !this.video.paused ? "" : "none";
this.root.querySelector("[data-play='pause']").style.display = this.video.paused ? "" : "none";
}
volumeChanged() {
if (this.video.hideAudioControls) {
for (let element of this.root.querySelectorAll("[data-volume]"))
element.style.display = "none";
this.volumeSlider.root.hidden = true;
} else {
let opacity = this.video.volume * 0.75 + 0.25;
this.root.querySelector("[data-volume='high']").style.display = !this.video.muted ? "" : "none";
this.root.querySelector("[data-volume='high']").style.opacity = opacity;
this.root.querySelector("[data-volume='mute']").style.display = this.video.muted ? "" : "none";
this.volumeSlider.root.hidden = false;
this.volumeSlider.setValue(this.video.muted ? 0 : this.video.volume);
}
}
timeChanged() {
if (this.video == null)
return;
let duration = this.video.duration;
let now = this.video.currentTime;
if (isNaN(duration)) {
this.time.innerText = "";
return;
}
if (duration < 10) {
let fmt = (totalSeconds) => {
let seconds = Math.floor(totalSeconds);
let ms = Math.round(totalSeconds * 1e3 % 1e3);
return "" + seconds + "." + ms.toString().padStart(3, "0");
};
this.time.innerText = \`\${fmt(now)} / \${fmt(duration)}\`;
} else {
this.time.innerText = \`\${helpers.strings.formatSeconds(now)} / \${helpers.strings.formatSeconds(duration)}\`;
}
}
};
var VolumeSliderWidget = class extends Widget {
constructor({
ondrag,
startedDragging,
stoppedDragging,
...options
}) {
super({
...options,
template: \`
<div class=volume-slider>
<div class=volume-line></div>
</div>
\`
});
this.ondrag = ondrag;
this.startedDragging = startedDragging;
this.stoppedDragging = stoppedDragging;
this.volumeLine = this.root.querySelector(".volume-line");
new PointerListener({
element: this.root,
callback: (e) => {
if (e.pressed) {
this.startedDragging();
this._capturedPointerId = e.pointerId;
this.root.setPointerCapture(this._capturedPointerId);
this.root.addEventListener("pointermove", this.pointermove);
this.handleDrag(e);
} else {
this.stopDragging();
}
}
});
}
get isDragging() {
return this._capturedPointerId != null;
}
pointermove = (e) => {
this.handleDrag(e);
};
stopDragging() {
this.stoppedDragging();
this.root.removeEventListener("pointermove", this.pointermove);
if (this._capturedPointerId != null) {
this.root.releasePointerCapture(this._capturedPointerId);
this._capturedPointerId = null;
}
}
setValue(value) {
if (this.isDragging)
return;
this.setValueInternal(value);
}
setValueInternal(value) {
value = 1 - value;
this.volumeLine.style.background = \`linear-gradient(to left, #000 \${value * 100}%, #FFF \${value * 100}px)\`;
}
handleDrag(e) {
let { left, width } = this.volumeLine.getBoundingClientRect();
let volume = (e.clientX - left) / width;
volume = Math.max(0, Math.min(1, volume));
this.setValueInternal(volume);
this.ondrag(volume);
}
};
/\x2f web/vview/viewer/video/viewer-video-base.js
var ViewerVideoBase = class extends Viewer {
constructor({ ...options }) {
super({ ...options, template: \`
<div class="viewer viewer-video">
<div class=top-seek-bar></div>
<div class=video-container></div>
<div class=video-ui-container></div>
</div>
\` });
this.videoContainer = this.root.querySelector(".video-container");
this.videoUi = new VideoUI({
container: this.root.querySelector(".video-ui-container")
});
this.videoUi.seekBar.setCurrentTime(0);
this.videoUi.seekBar.setCallback(this.seekCallback.bind(this));
if (ppixiv.mobile) {
this.topSeekBar = new SeekBar({
container: this.root.querySelector(".top-seek-bar")
});
this.seekDragger = new DragHandler({
name: "seek-dragger",
element: this.root,
deferDelayMs: 30,
...this._signal,
ondragstart: () => {
this.seekCallback(true, null);
this.dragRemainder = 0;
helpers.html.setClass(this.topSeekBar.root, "dragging", true);
return true;
},
ondrag: ({ movementX }) => {
let fraction = movementX / Math.min(window.innerWidth, window.innerHeight);
let currentTime = this._currentTime + this.dragRemainder;
let position = currentTime / this._duration;
position += fraction;
position = helpers.math.clamp(position, 0, 1);
let newPosition = position * this._duration;
this.seekCallback(true, newPosition);
this.dragRemainder = newPosition - this._currentTime;
},
ondragend: () => {
helpers.html.setClass(this.topSeekBar.root, "dragging", false);
this.seekCallback(false, null);
}
});
}
}
async load() {
let loadSentinel = this._loadSentinel = new Object();
this.mediaInfo = await ppixiv.mediaCache.getMediaInfo(this.mediaId);
return loadSentinel;
}
shutdown() {
this.mediaInfo = null;
this._loadSentinel = null;
this.video.remove();
this.videoUi.seekBar.setCallback(null);
super.shutdown();
}
refreshFocus() {
}
togglePause = (e) => {
this.setWantPlaying(!this.wantPlaying);
this.refreshFocus();
};
seekCallback(pause, seconds) {
this.seeking = pause;
}
setSeekBar({ currentTime = null, duration = null, available = null } = {}) {
if (currentTime != null)
this._currentTime = currentTime;
if (duration != null)
this._duration = duration;
if (available != null)
this.dragRemainder = null;
for (let bar of [this.videoUi.seekBar, this.topSeekBar]) {
if (bar == null)
continue;
if (currentTime != null)
bar.setCurrentTime(currentTime);
if (duration != null)
bar.setDuration(duration);
if (available != null)
bar.setLoaded(available);
}
}
};
/\x2f web/vview/viewer/video/viewer-video.js
var ViewerVideo = class extends ViewerVideoBase {
constructor({ ...options }) {
super({ ...options });
this.video = document.createElement("video");
this.video.loop = true;
this.video.controls = false;
this.video.preload = "auto";
this.video.playsInline = true;
this.video.volume = ppixiv.settings.get("volume");
this.video.muted = ppixiv.settings.get("mute");
this.video.inert = true;
this.video.addEventListener("volumechange", (e) => {
ppixiv.settings.set("volume", this.video.volume);
ppixiv.settings.set("mute", this.video.muted);
});
this.video.autoplay = true;
this.video.className = "filtering";
this.video.style.width = "100%";
this.video.style.height = "100%";
this.video.style.display = "block";
this.videoContainer.appendChild(this.video);
this.video.addEventListener("timeupdate", () => this.updateSeekBar());
this.video.addEventListener("progress", () => this.updateSeekBar());
this.videoContainer.addEventListener(ppixiv.mobile ? "dblclick" : "click", this.togglePause);
this.video.addEventListener("enterpictureinpicture", (e) => {
this._switchPosterToThumb();
});
let args = helpers.args.location;
this.wantPlaying = !args.state.paused;
this.seeking = false;
}
async load(mediaId, {
slideshow = false,
onnextimage = () => {
}
} = {}) {
await super.load(mediaId, { slideshow, onnextimage });
if (this.source)
this.source.remove();
this.source = document.createElement("source");
this.video.loop = !slideshow;
this.video.onended = () => {
onnextimage(this);
};
this.video.appendChild(this.source);
this.video.poster = this.mediaInfo.mangaPages[0].urls.poster;
this.source.src = this.mediaInfo.mangaPages[0].urls.original;
this.updateSeekBar();
this.video.load();
this.videoUi.videoChanged({ player: this, video: this.video });
let img = document.createElement("img");
img.src = this.video.poster;
let decode = img.decode();
let canplay = helpers.other.waitForEvent(this.video, "loadeddata");
await Promise.any([canplay, decode]);
this.ready.accept(true);
this.refreshFocus();
}
shutdown() {
super.shutdown();
if (this.source) {
this.source.remove();
this.source = null;
}
if (this.player) {
this.player.pause();
this.player = null;
}
}
set active(active) {
super.active = active;
this.refreshFocus();
}
_switchPosterToThumb() {
if (this.mediaInfo != null)
this.video.poster = this.mediaInfo.mangaPages[0].urls.small;
}
updateSeekBar() {
let currentTime = isNaN(this.video.currentTime) ? 0 : this.video.currentTime;
let duration = isNaN(this.video.duration) ? 1 : this.video.duration;
this.setSeekBar({ currentTime, duration });
}
toggleMute() {
this.video.muted = !this.video.muted;
}
onkeydown = (e) => {
if (this.video == null)
return;
if (e.code >= "Digit1" && e.code <= "Digit9") {
e.stopPropagation();
e.preventDefault();
if (!this.video)
return;
let speed;
switch (e.code) {
case "Digit1":
speed = 0.1;
break;
case "Digit2":
speed = 0.25;
break;
case "Digit3":
speed = 0.5;
break;
case "Digit4":
speed = 0.75;
break;
case "Digit5":
speed = 1;
break;
case "Digit6":
speed = 1.25;
break;
case "Digit7":
speed = 1.5;
break;
case "Digit8":
speed = 1.75;
break;
case "Digit9":
speed = 2;
break;
}
this.video.playbackRate = speed;
return;
}
switch (e.code) {
case "KeyM":
this.toggleMute();
break;
case "Space":
e.stopPropagation();
e.preventDefault();
this.setWantPlaying(!this.wantPlaying);
return;
case "Home":
e.stopPropagation();
e.preventDefault();
if (!this.video)
return;
this.video.currentTime = 0;
return;
case "End":
e.stopPropagation();
e.preventDefault();
if (!this.video)
return;
this.pause();
this.video.currentTime = this.video.duration - 1e-3;
return;
}
};
play() {
this.setWantPlaying(true);
}
pause() {
this.setWantPlaying(false);
}
setWantPlaying(value) {
if (this.wantPlaying != value) {
let args = helpers.args.location;
args.state.paused = !value;
helpers.navigate(args, { addToHistory: false, cause: "updating-video-pause" });
this.wantPlaying = value;
}
this.refreshFocus();
}
refreshFocus() {
super.refreshFocus();
if (this.source == null)
return;
let active = this.wantPlaying && !this.seeking && this._active;
if (active)
this.video.play();
else
this.video.pause();
}
seekCallback(pause, seconds) {
super.seekCallback(pause, seconds);
this.refreshFocus();
if (seconds != null) {
this.video.currentTime = seconds;
this.updateSeekBar();
this.videoUi.timeChanged();
}
}
};
/\x2f web/vview/widgets/zip-image-player.js
var ZipVideoInterface = class extends EventTarget {
constructor(player) {
super();
this.player = player;
}
get paused() {
return this.player.paused;
}
get duration() {
let result = this.player.getSeekableDuration();
if (result == null)
return NaN;
else
return result;
}
get currentTime() {
return this.player.getCurrentFrameTime();
}
play() {
return this.player.play();
}
pause() {
return this.player.pause();
}
hideAudioControls() {
return true;
}
};
var ZipImagePlayer = class {
constructor(options) {
this.op = options;
this.interface = new ZipVideoInterface(this);
this.waitingForFrame = true;
this.dead = false;
this.context = options.canvas.getContext("2d");
this.frame = 0;
this.failed = false;
this.frameTimestamps = [];
this.totalLength = 0;
this.frameCount = 0;
this.seekableLength = null;
this.frameData = [];
this.frameImages = [];
this.speed = 1;
this.paused = !this.op.autoStart;
this.load();
}
error(msg) {
this.failed = true;
throw Error("ZipImagePlayer error: " + msg);
}
async load() {
this.downloader = new ZipImageDownloader(this.op.source, {
signal: this.op.signal
});
if (this.op.local) {
let data;
try {
data = await this.downloader.getNextFrame();
} catch (e) {
console.info("Error downloading file", e);
return;
}
data = new Uint8Array(data);
data = String.fromCharCode.apply(null, data);
data = JSON.parse(data);
this.frameMetadata = data;
} else {
this.frameMetadata = this.op.metadata.frames;
}
this.frameTimestamps = [];
let milliseconds = 0;
let lastFrameTime = 0;
for (let frame2 of this.frameMetadata) {
this.frameTimestamps.push(milliseconds);
milliseconds += frame2.delay;
lastFrameTime = frame2.delay;
}
this.totalLength = milliseconds;
this.frameCount = this.frameMetadata.length;
this.seekableLength = milliseconds - lastFrameTime;
let frame = 0;
while (1) {
let file;
try {
file = await this.downloader.getNextFrame();
} catch (e) {
if (e.name != "AbortError")
console.info("Error downloading file", e);
return;
}
if (file == null)
break;
let mimeType = this.op.metadata?.mime_type || "image/jpeg";
let blob = new Blob([file], { type: mimeType });
this.frameData.push(blob);
if (this.op.progress) {
let progress = this.frameTimestamps[frame] / this.totalLength;
this.op.progress(progress);
}
frame++;
this._decodeFrames();
await helpers.other.sleep(1);
}
if (this.op.progress)
this.op.progress(null);
}
async _decodeFrames() {
if (this.loadingFrames)
return;
try {
this.loadingFrames = true;
while (await this._decodeOneFrame()) {
}
} finally {
this.loadingFrames = false;
}
}
async _decodeOneFrame() {
let ahead = 0;
for (ahead = 0; ahead < 2; ++ahead) {
let frame = this.frame + ahead;
let blob = this.frameData[frame];
if (blob == null)
return;
if (this.frameImages[frame])
continue;
let url = URL.createObjectURL(blob);
let image = document.createElement("img");
image.src = url;
await helpers.other.waitForImageLoad(image);
URL.revokeObjectURL(url);
this.frameImages[frame] = image;
if (this.waitingForFrame) {
this.waitingForFrame = false;
this._displayFrame();
}
if (this.dead)
return false;
return true;
}
return false;
}
async _displayFrame() {
if (this.dead)
return;
this._decodeFrames();
if (!this.frameImages[this.frame]) {
this.waitingForFrame = true;
return;
}
let image = this.frameImages[this.frame];
if (this.op.autosize) {
if (this.context.canvas.width != image.width || this.context.canvas.height != image.height) {
this.context.canvas.width = image.width;
this.context.canvas.height = image.height;
}
}
;
this.drawnFrame = this.frame;
this.context.clearRect(0, 0, this.op.canvas.width, this.op.canvas.height);
this.context.drawImage(image, 0, 0);
this.videoInterface.dispatchEvent(new Event("timeupdate"));
if (this.paused)
return;
let meta = this.frameMetadata[this.frame];
this.pendingFrameMetadata = meta;
this._refreshTimer();
}
_unsetTimer() {
if (!this.timer)
return;
realClearTimeout(this.timer);
this.timer = null;
}
_refreshTimer() {
if (this.paused)
return;
this._unsetTimer();
this.timer = realSetTimeout(this._nextFrame, this.pendingFrameMetadata.delay / this.speed);
}
_getFrameDuration() {
let meta = this.frameMetadata[this.frame];
return meta.delay;
}
_nextFrame = (frame) => {
this.timer = null;
if (this.frame >= this.frameCount - 1) {
if (!this.op.loop) {
this.pause();
if (this.op.onfinished)
this.op.onfinished();
return;
}
this.frame = 0;
} else {
this.frame += 1;
}
this._displayFrame();
};
play() {
if (this.dead)
return;
if (this.paused) {
this.paused = false;
this._displayFrame();
this.videoInterface.dispatchEvent(new Event("play"));
}
}
pause() {
if (this.dead)
return;
if (!this.paused) {
this._unsetTimer();
this.paused = true;
this.videoInterface.dispatchEvent(new Event("pause"));
}
}
_setPause(value) {
if (this.dead)
return;
if (this.paused = value)
return;
this.context.canvas.paused = this.paused;
this.paused = value;
}
get videoInterface() {
return this.interface;
}
togglePause() {
if (this.paused)
this.play();
else
this.pause();
}
rewind() {
if (this.dead)
return;
this.frame = 0;
this._unsetTimer();
this._displayFrame();
}
setSpeed(value) {
this.speed = value;
this._refreshTimer();
}
stop() {
this.dead = true;
this._unsetTimer();
this.frameImages = null;
}
getCurrentFrame() {
return this.frame;
}
setCurrentFrame(frame) {
frame %= this.frameCount;
if (frame < 0)
frame += this.frameCount;
this.frame = frame;
this._displayFrame();
}
getTotalDuration() {
return this.totalLength / 1e3;
}
getSeekableDuration() {
if (this.seekableLength == null)
return null;
else
return this.seekableLength / 1e3;
}
getCurrentFrameTime() {
let timestamp = this.frameTimestamps[this.frame];
return timestamp == null ? null : timestamp / 1e3;
}
setCurrentFrameTime(seconds) {
let closestFrame = null;
let closestError = null;
for (let frame = 0; frame < this.frameMetadata.length; ++frame) {
if (!this.frameData[frame])
break;
let error = Math.abs(seconds - this.frameTimestamps[frame] / 1e3);
if (closestFrame == null || error < closestError) {
closestFrame = frame;
closestError = error;
}
}
this.frame = closestFrame;
this._displayFrame();
}
getFrameCount() {
return this.frameCount;
}
};
/\x2f web/vview/viewer/video/viewer-ugoira.js
var ViewerUgoira = class extends ViewerVideoBase {
constructor({ ...options }) {
super({ ...options });
this.video = document.createElement("canvas");
this.video.hidden = true;
this.video.className = "filtering";
this.video.style.width = "100%";
this.video.style.height = "100%";
this.video.style.objectFit = "contain";
this.videoContainer.appendChild(this.video);
this.video.addEventListener(ppixiv.mobile ? "dblclick" : "click", this.togglePause);
let args = helpers.args.location;
this.wantPlaying = !args.state.paused;
this.seeking = false;
window.addEventListener("visibilitychange", this.refreshFocus.bind(this), { signal: this.shutdownSignal });
}
async load() {
let local = helpers.mediaId.isLocal(this.mediaId);
let partialMediaInfo = await ppixiv.mediaCache.getMediaInfoSync(this.mediaId, { full: false });
if (partialMediaInfo) {
if (local)
this._createPreviewImage(partialMediaInfo.mangaPages[0].urls.poster, null);
else
this._createPreviewImage(partialMediaInfo.previewUrls[0], null);
helpers.other.waitForImageLoad(this.previewImage).then(() => this.ready.accept(true));
} else {
this.ready.accept(true);
}
let { slideshow = false, onnextimage = null } = this.options;
let loadSentinel = await super.load(this.mediaId, {
slideshow,
onnextimage: () => onnextimage(this)
});
if (loadSentinel !== this._loadSentinel)
return;
this.abortController = new AbortController();
let source = null;
if (local) {
source = this.mediaInfo.mangaPages[0].urls.mjpeg_zip;
} else {
source = this.mediaInfo.ugoiraMetadata.originalSrc;
}
this.player = new ZipImagePlayer({
metadata: this.mediaInfo.isLocal ? null : this.mediaInfo.ugoiraMetadata,
autoStart: false,
source,
local,
mime_type: this.mediaInfo.isLocal ? null : this.mediaInfo.ugoiraMetadata?.mime_type,
signal: this.abortController.signal,
autosize: true,
canvas: this.video,
loop: !slideshow,
progress: this.progress,
onfinished: () => onnextimage(this)
});
this.player.videoInterface.addEventListener("timeupdate", this.ontimeupdate, { signal: this.abortController.signal });
this.videoUi.videoChanged({ player: this, video: this.player.videoInterface });
this.refreshFocus();
}
shutdown() {
super.shutdown();
if (this.abortController) {
this.abortController.abort();
this.abortController = null;
}
this.progress(null);
this.video.hidden = true;
if (this.player) {
this.player.pause();
this.player = null;
}
if (this.previewImage) {
this.previewImage.remove();
this.previewImage = null;
}
}
async _createPreviewImage(url) {
if (this.previewImage) {
this.previewImage.remove();
this.previewImage = null;
}
let img = document.createElement("img");
img.classList.add("low-res-preview");
img.style.position = "absolute";
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "contain";
img.src = url ?? helpers.other.blankImage;
this.videoContainer.appendChild(img);
this.previewImage = img;
img.addEventListener(ppixiv.mobile ? "dblclick" : "click", this.togglePause);
}
set active(active) {
super.active = active;
this.refreshFocus();
}
progress = (available) => {
available ??= 1;
this.setSeekBar({ available });
};
ontimeupdate = () => {
if (this.previewImage)
this.previewImage.hidden = true;
this.video.hidden = false;
this.updateSeekBar();
};
updateSeekBar() {
let currentTime = this.player.getCurrentFrameTime();
let duration = this.player.getSeekableDuration();
this.setSeekBar({ currentTime, duration });
}
onkeydown = (e) => {
if (e.code >= "Digit1" && e.code <= "Digit9") {
e.stopPropagation();
e.preventDefault();
if (!this.player)
return;
let speed;
switch (e.code) {
case "Digit1":
speed = 0.1;
break;
case "Digit2":
speed = 0.25;
break;
case "Digit3":
speed = 0.5;
break;
case "Digit4":
speed = 0.75;
break;
case "Digit5":
speed = 1;
break;
case "Digit6":
speed = 1.25;
break;
case "Digit7":
speed = 1.5;
break;
case "Digit8":
speed = 1.75;
break;
case "Digit9":
speed = 2;
break;
}
this.player.setSpeed(speed);
return;
}
switch (e.code) {
case "Space":
e.stopPropagation();
e.preventDefault();
this.setWantPlaying(!this.wantPlaying);
return;
case "Home":
e.stopPropagation();
e.preventDefault();
if (!this.player)
return;
this.player.rewind();
return;
case "End":
e.stopPropagation();
e.preventDefault();
if (!this.player)
return;
this.pause();
this.player.setCurrentFrame(this.player.getFrameCount() - 1);
return;
case "KeyQ":
case "KeyW":
e.stopPropagation();
e.preventDefault();
if (!this.player)
return;
this.pause();
let currentFrame = this.player.getCurrentFrame();
let next = e.code == "KeyW";
let newFrame = currentFrame + (next ? 1 : -1);
this.player.setCurrentFrame(newFrame);
return;
}
};
play() {
this.setWantPlaying(true);
}
pause() {
this.setWantPlaying(false);
}
setWantPlaying(value) {
if (this.wantPlaying != value) {
let args = helpers.args.location;
args.state.paused = !value;
helpers.navigate(args, { addToHistory: false, cause: "updating-video-pause" });
this.wantPlaying = value;
}
this.refreshFocus();
}
refreshFocus() {
super.refreshFocus();
if (this.player == null)
return;
let active = this.wantPlaying && !this.seeking && !window.document.hidden && this._active;
if (active)
this.player.play();
else
this.player.pause();
}
seekCallback(pause, seconds) {
super.seekCallback(pause, seconds);
this.refreshFocus();
if (seconds != null)
this.player.setCurrentFrameTime(seconds);
}
};
/\x2f web/vview/viewer/viewer-error.js
var ViewerError = class extends Viewer {
constructor({
...options
} = {}) {
super({ ...options, template: \`
<div class="viewer viewer-error">
<img class=muted-image>
<div class=error-text-container>
<span class=muted-label hidden>Muted:</span>
<span class=error-text></span>
<div class=view-muted-image hidden>
View image
</div>
</div>
</div>
\` });
this.root.querySelector(".view-muted-image").addEventListener("click", (e) => {
let args = helpers.args.location;
args.hash.set("view-muted", "1");
helpers.navigate(args, { addToHistory: false, cause: "override-mute" });
});
this.errorText = this.root.querySelector(".error-text");
this.ready.accept(true);
}
async load() {
let { error, slideshow = false, onnextimage = () => {
} } = this.options;
if (slideshow && onnextimage) {
let slideshowTimer = this._slideshowTimer = (async () => {
await helpers.other.sleep(2e3);
if (slideshowTimer != this._slideshowTimer)
return;
onnextimage(this);
})();
}
if (error) {
console.log("Showing error view:", error);
this.errorText.innerText = error;
return;
}
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(this.mediaId);
let userInfo = await ppixiv.userCache.getUserInfo(mediaInfo.userId);
if (userInfo) {
let img = this.root.querySelector(".muted-image");
img.src = userInfo.imageBig;
}
let mutedTag = ppixiv.muting.anyTagMuted(mediaInfo.tagList);
let mutedUser = ppixiv.muting.isUserIdMuted(mediaInfo.userId);
this.root.querySelector(".muted-label").hidden = false;
this.root.querySelector(".view-muted-image").hidden = false;
if (mutedTag) {
let translatedTag = await ppixiv.tagTranslations.getTranslation(mutedTag);
this.errorText.innerText = translatedTag;
} else if (mutedUser)
this.errorText.innerText = mediaInfo.userName;
}
shutdown() {
super.shutdown();
this._slideshowTimer = null;
}
};
/\x2f web/vview/misc/image-preloader.js
var ImagePreloader = class {
static get singleton() {
if (ImagePreloader._singleton == null)
ImagePreloader._singleton = new ImagePreloader();
return ImagePreloader._singleton;
}
constructor() {
this.preloads = [];
this.recentlyPreloadedUrls = [];
}
async setCurrentImage(mediaId) {
if (this.currentMediaId == mediaId)
return;
this.currentMediaId = mediaId;
this.currentMediaInfo = null;
await this.guessPreload(mediaId);
if (this.currentMediaId == null)
return;
let illustInfo = await ppixiv.mediaCache.getMediaInfo(this.currentMediaId);
if (this.currentMediaId != mediaId)
return;
this.currentMediaInfo = illustInfo;
this.checkFetchQueue();
}
async setSpeculativeImage(mediaId) {
if (this._speculativeMediaId == mediaId)
return;
this._speculativeMediaId = mediaId;
this._speculativeMediaInfo = null;
if (this._speculativeMediaId == null)
return;
let illustInfo = await ppixiv.mediaCache.getMediaInfo(this._speculativeMediaId);
if (this._speculativeMediaId != mediaId)
return;
if (this._speculativeMediaId != mediaId)
return;
this._speculativeMediaInfo = illustInfo;
this.checkFetchQueue();
}
checkFetchQueue() {
let wantedPreloads = [];
if (this.currentMediaInfo != null)
wantedPreloads = wantedPreloads.concat(this._createPreloadersForIllust(this.currentMediaInfo, this.currentMediaId));
if (this._speculativeMediaInfo != null)
wantedPreloads = wantedPreloads.concat(this._createPreloadersForIllust(this._speculativeMediaInfo, this._speculativeMediaId));
let filteredPreloads = [];
for (let preload of wantedPreloads) {
if (this.recentlyPreloadedUrls.indexOf(preload.url) == -1)
filteredPreloads.push(preload);
}
if (filteredPreloads.length == 0) {
return;
}
let concurrentPreloads = 5;
filteredPreloads.splice(concurrentPreloads);
for (let preload of filteredPreloads) {
let activePreload = this._findActivePreloadByUrl(preload.url);
if (activePreload != null)
return;
}
let updatedPreloadList = [];
for (let preload of filteredPreloads) {
let promise = preload.start();
let aborted = false;
promise.catch((e) => {
if (e.name == "AbortError")
aborted = true;
});
promise.finally(() => {
if (!aborted) {
this.recentlyPreloadedUrls.push(preload.url);
this.recentlyPreloadedUrls.splice(0, this.recentlyPreloadedUrls.length - 1e3);
}
let idx = this.preloads.indexOf(preload);
if (idx == -1) {
console.error("Preload finished, but we weren't running it:", preload.url);
return;
}
this.preloads.splice(idx, 1);
this.checkFetchQueue();
});
updatedPreloadList.push(preload);
break;
}
for (let preload of this.preloads) {
if (updatedPreloadList.indexOf(preload) != -1)
continue;
preload.cancel();
updatedPreloadList.push(preload);
}
this.preloads = updatedPreloadList;
}
_findActivePreloadByUrl(url) {
for (let preload of this.preloads)
if (preload.url == url)
return preload;
return null;
}
_createPreloadersForIllust(mediaInfo, mediaId) {
if (ppixiv.muting.anyTagMuted(mediaInfo.tagList))
return [];
if (ppixiv.muting.isUserIdMuted(mediaInfo.userId))
return [];
if (mediaInfo.illustType == 2 && !helpers.mediaId.isLocal(mediaId)) {
let results2 = [];
let firefox = navigator.userAgent.indexOf("Firefox/") != -1;
if (!firefox)
results2.push(new FetchResourceLoader(mediaInfo.ugoiraMetadata.originalSrc));
results2.push(new ImgResourceLoader([mediaInfo.mangaPages[0].urls.original]));
return results2;
}
if (mediaInfo.illustType == "video")
return [new ImgResourceLoader([mediaInfo.mangaPages[0].urls.poster])];
let results = [];
let previewUrls = [];
for (let url of mediaInfo.previewUrls) {
if (!LocalAPI.shouldPreloadThumbs(mediaId, url))
continue;
previewUrls.push(url);
}
if (previewUrls.length > 0)
results.push(new ImgResourceLoader(previewUrls));
let page = helpers.mediaId.parse(mediaId).page;
if (page < mediaInfo.mangaPages.length) {
let { url } = mediaInfo.getMainImageUrl(page);
results.push(new ImgResourceLoader([url]));
}
let preloadMode = ppixiv.settings.get("preload_manga");
if (preloadMode == "partial" || preloadMode == "full") {
for (let p = 0; p < mediaInfo.mangaPages.length; ++p) {
if (p == page)
continue;
let staggered = p < page - 2 || p >= page + 2;
if (preloadMode == "partial" && staggered)
continue;
let { url } = mediaInfo.getMainImageUrl(p);
results.push(new ImgResourceLoader([url], { staggered }));
}
}
return results;
}
async guessPreload(mediaId) {
if (ppixiv.mobile)
return;
let guessedUrl = null;
if (mediaId != null) {
guessedUrl = await ppixiv.guessImageUrl.guessUrl(mediaId);
if (this.guessedPreload && this.guessedPreload.url == guessedUrl)
return;
}
if (this.guessedPreload) {
this.guessedPreload.cancel();
this.guessedPreload = null;
}
if (guessedUrl) {
this.guessedPreload = new ImgResourceLoader([guessedUrl], {
onerror: () => {
ppixiv.guessImageUrl.guessedUrlIncorrect(mediaId);
}
});
this.guessedPreload.start();
}
}
};
var StaggerDelay = 1500;
var _ResourceLoader = class {
constructor({
staggered = false
} = {}) {
this.staggered = staggered;
this.abortController = new AbortController();
}
get aborted() {
return this.abortController.signal.aborted;
}
async start() {
await this._waitForStaggerDelay();
this._startedAt = Date.now();
}
async _waitForStaggerDelay() {
if (_ResourceLoader.lastLoadFinishTime == null)
return;
let staggerLoad = this.staggered;
if (document.visibilityState == "hidden")
staggerLoad = true;
if (!staggerLoad)
return;
let timeSinceLastLoad = Date.now() - _ResourceLoader.lastLoadFinishTime;
let ms = StaggerDelay - timeSinceLastLoad;
if (ms > 0) {
await helpers.other.sleep(ms);
}
}
_loadFinished() {
if (this.aborted)
return;
let loadTook = Date.now() - this._startedAt;
let wasCached = loadTook < 250;
if (!wasCached)
_ResourceLoader.lastLoadFinishTime = Date.now();
}
cancel() {
this.abortController.abort();
}
};
var ResourceLoader = _ResourceLoader;
__publicField(ResourceLoader, "lastLoadFinishTime", null);
var ImgResourceLoader = class extends ResourceLoader {
constructor(urls, {
onerror = null,
...args
} = {}) {
super({ ...args });
this.urls = urls;
console.assert(this.urls.length > 0);
this.url = urls[0];
this.onerror = onerror;
}
async start() {
await super.start();
if (this.aborted)
return;
let promises = [];
for (let url of this.urls) {
let img = document.createElement("img");
img.src = url;
let promise = helpers.other.waitForImageLoad(img, this.abortController.signal);
;
promises.push(promise);
}
let isFirstError = true;
for (let promise of promises) {
let result = await promise;
if (result == "failed" && this.onerror && isFirstError) {
isFirstError = false;
this.onerror();
}
}
this._loadFinished();
}
};
var FetchResourceLoader = class extends ResourceLoader {
constructor(url, args) {
super(args);
this.url = url;
console.assert(url);
}
async start() {
if (this.url == null)
return;
await super.start();
if (this.aborted)
return;
let request = helpers.pixivRequest.sendPixivRequest({
url: this.url,
method: "GET",
signal: this.abortController.signal
});
try {
request = await request;
await request.text();
} catch (e) {
}
this._loadFinished();
}
};
/\x2f web/vview/screen-illust/screen-illust.js
var ScreenIllust = class extends Screen {
get screenType() {
return "illust";
}
constructor(options) {
super({ ...options, template: \`
<div inert class="screen screen-illust-container">
<!-- This holds our views: the current view, and the neighbor view if we're transitioning
between them. -->
<div class="view-container mouse-hidden-box" data-context-menu-target></div>
<div class=manga-page-indicator>
<div class=bar></div>
</div>
<div class=page-change-indicator data-icon=last-image>
<ppixiv-inline src="resources/last-page.svg"></ppixiv-inline>
</div>
<!-- The top-left hover UI is inserted here. -->
<div class=ui>
</div>
<!-- This is displayed while loading translations for the current image. -->
<div class=translation-status>
<div class=translation-contents>\${helpers.createIcon("mat:translate")}</div>
</div>
<div class=fade-search></div>
</div>
\` });
this.currentMediaId = null;
this.latestNavigationDirectionDown = true;
let uiContainer = this.root.querySelector(".ui");
if (!ppixiv.mobile)
this.desktopUi = new DesktopImageInfo({ container: uiContainer });
if (ppixiv.mobile)
uiContainer.hidden = true;
ppixiv.userCache.addEventListener("usermodified", this.refreshUi, { signal: this.shutdownSignal });
ppixiv.mediaCache.addEventListener("mediamodified", this.refreshUi, { signal: this.shutdownSignal });
ppixiv.settings.addEventListener("recent-bookmark-tags", this.refreshUi, { signal: this.shutdownSignal });
this.viewContainer = this.root.querySelector(".view-container");
let pageChangeIndicator = this.root.querySelector(".page-change-indicator");
pageChangeIndicator.addEventListener("animationend", (e) => {
pageChangeIndicator.classList.remove("flash");
});
this.mangaPageIndicator = this.root.querySelector(".manga-page-indicator");
if (!ppixiv.mobile) {
this.viewContainer.addEventListener("dblclick", () => {
helpers.toggleFullscreen();
});
new HideMouseCursorOnIdle(this.root.querySelector(".mouse-hidden-box"));
this.root.addEventListener("wheel", this.onwheel, { passive: false });
}
if (ppixiv.mobile) {
this.mobileImageChanger = new DragImageChanger({ parent: this });
this.mobileIllustUi = new MobileImageUI({
container: this.root,
transitionTarget: this.root
});
this.root.addEventListener("dblclick", (e) => this.viewer.toggleZoom(e), this._signal);
new IsolatedTapHandler({
parent: this,
node: this.viewContainer,
callback: (e) => {
this.mobileIllustUi.toggle();
}
});
}
this.mobileImageDismiss = new MobileImageDismiss({ parent: this });
this.deactivate();
}
setDataSource(dataSource) {
if (dataSource == this.dataSource)
return;
if (this.dataSource != null) {
this.dataSource.removeEventListener("updated", this.dataSourceUpdated);
this.dataSource = null;
}
this.dataSource = dataSource;
if (this.desktopUi)
this.desktopUi.dataSource = dataSource;
if (this.dataSource != null) {
this.dataSource.addEventListener("updated", this.dataSourceUpdated);
this.refreshUi();
}
}
async activate({ mediaId, restoreHistory, cause }) {
let wasActive = this._active;
this._active = true;
super.activate();
if (this.viewer != null)
this.viewer.active = true;
if (this.mobileImageChanger)
this.mobileImageChanger.stop();
await this.showImage(mediaId, { restoreHistory, initial: !wasActive });
if (this.mobileImageDismiss)
this.mobileImageDismiss.activate({ cause });
}
deactivate() {
super.deactivate();
this._active = false;
if (this.viewer != null)
this.viewer.active = false;
if (this.mobileImageChanger)
this.mobileImageChanger.stop();
this.cancelAsyncNavigation();
if (this.mobileIllustUi) {
this.mobileIllustUi.mediaId = null;
this.mobileIllustUi.setDataSource(null);
}
if (this.mobileImageDismiss)
this.mobileImageDismiss.deactivate();
this.cleanupImage();
ppixiv.settings.set("image_editing_mode", null);
}
cleanupImage() {
if (this._active)
return;
if (this.mobileImageDismiss?.isAnimating)
return;
this.removeViewer();
this._wantedMediaId = null;
this.currentMediaId = null;
this.refreshUi();
ImagePreloader.singleton.setCurrentImage(null);
ImagePreloader.singleton.setSpeculativeImage(null);
if (ppixiv.settings.get("linked_tabs_enabled")) {
ppixiv.sendImage.sendMessage({
message: "send-image",
action: "cancel",
to: ppixiv.settings.get("linked_tabs", [])
});
}
}
createViewer({ mediaId, earlyIllustData, ...options } = {}) {
let viewerClass;
let isMuted = earlyIllustData && this.shouldHideMutedImage(earlyIllustData).isMuted;
let isError = earlyIllustData == null;
if (isMuted) {
viewerClass = ViewerError;
} else if (isError) {
viewerClass = ViewerError;
let error = ppixiv.mediaCache.getMediaLoadError(mediaId);
error ??= "Unknown error";
options = { ...options, error };
} else if (earlyIllustData.illustType == 2)
viewerClass = ViewerUgoira;
else if (earlyIllustData.illustType == "video")
viewerClass = ViewerVideo;
else
viewerClass = ppixiv.mobile ? ViewerImagesMobile : ViewerImagesDesktop;
let newViewer = new viewerClass({
mediaId,
container: this.viewContainer,
slideshow: helpers.args.location.hash.get("slideshow"),
waitForTransitions: () => {
return this.mobileImageDismiss?.waitForAnimationsPromise;
},
onnextimage: async (finishedViewer) => {
if (!this._active)
return {};
if (finishedViewer != this.viewer) {
console.log("onnextimage from viewer that isn't active");
return {};
}
let manga = ppixiv.settings.get("slideshow_skips_manga") ? "skip-to-first" : "normal";
return await this.navigateToNext(1, { flashAtEnd: false, manga });
},
...options
});
newViewer.load();
return newViewer;
}
async showImage(mediaId, { restoreHistory = false, initial = false } = {}) {
console.assert(mediaId != null);
this.cancelAsyncNavigation();
this._wantedMediaId = mediaId;
if (await this.loadFirstImage(mediaId))
return;
let earlyIllustData = await ppixiv.mediaCache.getMediaInfo(mediaId, { full: false });
if (!this.active || this._wantedMediaId != mediaId) {
console.log("showImage: illust ID or page changed while async, stopping");
return;
}
if (this.mobileImageChanger)
this.mobileImageChanger.stop();
let newViewer = this.createViewer({
earlyIllustData,
mediaId,
restoreHistory
});
this.showImageViewer({ newViewer, initial });
}
showImageViewer({ newViewer = null, initial = false } = {}) {
if (newViewer == this.viewer)
return;
helpers.html.setClass(document.body, "force-ui", window.debugShowUi);
let mediaId = newViewer.mediaId;
if (this.currentMediaId != mediaId)
ppixiv.message.hide();
this._wantedMediaId = mediaId;
this.currentMediaId = mediaId;
let earlyIllustData = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (this.active)
helpers.setTitleAndIcon(earlyIllustData);
helpers.html.setClass(document.body, "dot", helpers.pixiv.tagsContainDot(earlyIllustData?.tagList));
this.showCurrentPage();
if (ppixiv.settings.get("linked_tabs_enabled"))
ppixiv.sendImage.send_image(mediaId, ppixiv.settings.get("linked_tabs", []), "temp-view");
if (helpers.mediaId.isLocal(mediaId))
ppixiv.mediaCache.refreshMediaInfo(mediaId);
ImagePreloader.singleton.setCurrentImage(mediaId);
if (!initial || helpers.mediaId.isLocal(mediaId)) {
(async () => {
let newMediaId = await this.getNavigation(this.latestNavigationDirectionDown);
ImagePreloader.singleton.setSpeculativeImage(newMediaId);
})();
}
this.refreshUi();
if (this.mobileImageDismiss)
this.mobileImageDismiss.scrollSearchToThumbnail();
if (this.oldViewer && this.viewer) {
this.viewer.shutdown();
this.viewer = null;
} else
this.oldViewer = this.viewer;
this.viewer = newViewer;
let oldViewer = this.oldViewer;
if (this.oldViewer)
this.viewer.visible = false;
this.viewer.ready.finally(() => {
if (this.viewer !== newViewer || oldViewer !== this.oldViewer)
return;
this.viewer.visible = true;
if (this.oldViewer) {
this.oldViewer.shutdown();
this.oldViewer = null;
}
});
this.viewer.active = this._active;
this.refreshUi();
}
takeViewer() {
let viewer = this.viewer;
this.viewer = null;
return viewer;
}
async loadFirstImage(mediaId) {
if (helpers.mediaId.isLocal(mediaId)) {
let args = helpers.args.location;
LocalAPI.getArgsForId(mediaId, args);
if (args.hash.get("file") != "*")
return false;
} else if (helpers.mediaId.parse(mediaId).id != "*")
return false;
let newMediaId = await this.dataSource.getOrLoadNeighboringMediaId(null, true);
if (newMediaId == null) {
ppixiv.message.show("Couldn't find an image to view");
return true;
}
ppixiv.app.showMediaId(newMediaId, {
addToHistory: false
});
return true;
}
get viewMuted() {
return helpers.args.location.hash.get("view-muted") == "1";
}
shouldHideMutedImage(earlyIllustData) {
let mutedTag = ppixiv.muting.anyTagMuted(earlyIllustData.tagList);
let mutedUser = ppixiv.muting.isUserIdMuted(earlyIllustData.userId);
if (this.viewMuted || !mutedTag && !mutedUser)
return { isMuted: false };
return { isMuted: true, mutedTag, mutedUser };
}
removeViewer() {
if (this.viewer != null) {
this.viewer.shutdown();
this.viewer = null;
}
if (this.oldViewer != null) {
this.oldViewer.shutdown();
this.oldViewer = null;
}
}
cancelAsyncNavigation() {
if (this.pendingNavigation == null)
return;
console.debug("Reach end, cancelling async navigation");
this.pendingNavigation = null;
}
dataSourceUpdated = () => {
this.refreshUi();
};
get active() {
return this._active;
}
refreshUi = (e) => {
if (!this._active)
return;
if (this.desktopUi)
this.desktopUi.mediaId = this.currentMediaId;
if (this.mobileIllustUi) {
this.mobileIllustUi.mediaId = this.currentMediaId;
this.mobileIllustUi.setDataSource(this.dataSource);
}
if (this.desktopUi)
this.desktopUi.refresh();
};
onwheel = (e) => {
if (!this._active)
return;
let down = e.deltaY > 0;
this.navigateToNext(down, { manga: e.shiftKey ? "skip-to-first" : "normal" });
};
get displayedMediaId() {
return this._wantedMediaId;
}
handleKeydown(e) {
if (this.viewer && this.viewer.onkeydown) {
this.viewer.onkeydown(e);
if (e.defaultPrevented)
return;
}
if (this.desktopUi)
this.desktopUi.handleKeydown(e);
if (e.defaultPrevented)
return;
if (e.ctrlKey || e.altKey || e.metaKey)
return;
switch (e.key) {
case "ArrowLeft":
case "ArrowUp":
case "PageUp":
e.preventDefault();
e.stopPropagation();
this.navigateToNext(false, { manga: e.shiftKey ? "skip-to-first" : "normal" });
break;
case "ArrowRight":
case "ArrowDown":
case "PageDown":
e.preventDefault();
e.stopPropagation();
this.navigateToNext(true, { manga: e.shiftKey ? "skip-to-first" : "normal" });
break;
}
}
async getNavigation(down, { navigateFromMediaId = null, manga = "normal", loop = false } = {}) {
navigateFromMediaId ??= this._wantedMediaId;
navigateFromMediaId ??= this.currentMediaId;
if (!loop)
return await this.dataSource.getOrLoadNeighboringMediaId(navigateFromMediaId, down, { manga });
let mediaId = await this.dataSource.getOrLoadNeighboringMediaIdWithLoop(navigateFromMediaId, down, { manga });
if (mediaId == navigateFromMediaId) {
console.log("Not looping since we only have one media ID");
return null;
}
return mediaId;
}
async navigateToNext(down, { manga = "normal", flashAtEnd = true } = {}) {
let loop = helpers.args.location.hash.get("slideshow") != null;
if (manga == "normal" && this.viewer instanceof ViewerError)
manga = "skip-past";
this.latestNavigationDirectionDown = down;
this.cancelAsyncNavigation();
let pendingNavigation = this.pendingNavigation = new Object();
let newMediaId = await this.getNavigation(down, { manga, loop });
if (newMediaId == null) {
console.log("Reached the end of the list");
if (flashAtEnd)
this.flashEndIndicator(down, "last-image");
return { reachedEnd: true };
}
if (this.pendingNavigation != pendingNavigation) {
console.error("Aborting stale navigation");
return { stale: true };
}
this.pendingNavigation = null;
ppixiv.app.showMediaId(newMediaId);
return { mediaId: newMediaId };
}
flashEndIndicator(down, icon) {
let indicator = this.root.querySelector(".page-change-indicator");
indicator.dataset.icon = icon;
indicator.dataset.side = down ? "right" : "left";
indicator.classList.remove("flash");
indicator.getAnimations();
indicator.classList.add("flash");
}
async showCurrentPage() {
if (!ppixiv.settings.get("manga_page_indicator"))
return;
if (helpers.args.location.hash.get("slideshow"))
return;
if (this._lastShownMangaPageId == this.currentMediaId)
return;
this._lastShownMangaPageId = this.currentMediaId;
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(this.currentMediaId, { full: false });
let ourSentinel = this._hideCurrentPageSentinel = new Object();
if (this._hideCurrentPageTimer !== null)
realClearTimeout(this._hideCurrentPageTimer);
if (mediaInfo != null && mediaInfo.pageCount >= 2) {
let pageCount = mediaInfo.pageCount;
let [_, page] = helpers.mediaId.toIllustIdAndPage(this.currentMediaId);
let percent = Math.round(helpers.math.scale(page, 0, pageCount - 1, 0, 100));
this.mangaPageIndicator.style.setProperty("--percent", \`\${percent}%\`);
if (this._mangaPageIndicatorAnimation) {
this._mangaPageIndicatorAnimation.cancel();
this._mangaPageIndicatorAnimation = null;
}
this.mangaPageIndicator.style.opacity = 1;
await helpers.other.sleep(250);
}
if (ourSentinel != this._hideCurrentPageSentinel)
return;
this._mangaPageIndicatorAnimation = this.mangaPageIndicator.animate([
{ opacity: 0 }
], {
duration: 150,
easing: "ease-in",
fill: "forwards"
});
try {
await this._mangaPageIndicatorAnimation.finished;
} catch {
return;
}
if (ourSentinel != this._hideCurrentPageSentinel)
return;
this._mangaPageIndicatorAnimation.commitStyles();
this._mangaPageIndicatorAnimation.cancel();
this._mangaPageIndicatorAnimation = null;
}
};
/\x2f web/vview/screen-search/search-menu.js
function getMainSearchMenuOptions() {
if (ppixiv.native)
return [
{ label: "Files", icon: "search", url: \`/#/\` },
{ label: "Similar Images", icon: "search", url: \`/similar#/\`, visible: false, classes: ["disable-clicks"] }
];
let options = [
{ label: "Artist", icon: "face", url: "/users/1#ppixiv", visible: false, classes: ["artist-row", "disable-clicks"] },
{ label: "Search works", icon: "search", url: \`/tags/\x2fartworks#ppixiv\` },
{ label: "New works by following", icon: "photo_library", url: "/bookmark_new_illust.php#ppixiv" },
{ label: "New works by everyone", icon: "groups", url: "/new_illust.php#ppixiv" }
];
if (ppixiv.mobile) {
options = [
...options,
{ label: "Bookmarks", icon: "favorite", url: \`/users/\${ppixiv.pixivInfo.userId}/bookmarks/artworks#ppixiv\` },
{ label: "Followed users", icon: "visibility", url: \`/users/\${ppixiv.pixivInfo.userId}/following#ppixiv\` }
];
} else {
options = [
...options,
[
{ label: "Bookmarks", icon: "favorite", url: \`/users/\${ppixiv.pixivInfo.userId}/bookmarks/artworks#ppixiv\` },
{ label: "all", url: \`/users/\${ppixiv.pixivInfo.userId}/bookmarks/artworks#ppixiv\` },
{ label: "Public", url: \`/users/\${ppixiv.pixivInfo.userId}/bookmarks/artworks#ppixiv?show-all=0\` },
{ label: "Private", url: \`/users/\${ppixiv.pixivInfo.userId}/bookmarks/artworks?rest=hide#ppixiv?show-all=0\` }
],
[
{ label: "Followed users", icon: "visibility", url: \`/users/\${ppixiv.pixivInfo.userId}/following#ppixiv\` },
{ label: "Public", url: \`/users/\${ppixiv.pixivInfo.userId}/following#ppixiv\` },
{ label: "Private", url: \`/users/\${ppixiv.pixivInfo.userId}/following?rest=hide#ppixiv\` }
]
];
}
options = [
...options,
{ label: "Rankings", icon: "auto_awesome", url: "/ranking.php#ppixiv" },
{ label: "Recommended works", icon: "ppixiv:suggestions", url: "/discovery#ppixiv" },
{ label: "Recommended users", icon: "ppixiv:suggestions", url: "/discovery/users#ppixiv" },
{ label: "Completed requests", icon: "request_page", url: "/request/complete/illust#ppixiv" },
{ label: "Users", icon: "search", url: "/search_user.php#ppixiv" }
];
return options;
}
function CreateSearchMenu(container) {
let options = getMainSearchMenuOptions();
let createOption = ({ classes = [], ...options2 }) => {
let button = new MenuOptionButton({
classes: [...classes, "navigation-button"],
...options2
});
return button;
};
for (let option of options) {
if (Array.isArray(option)) {
let row = new MenuOptionRow({
container
});
let first = true;
for (let suboption of option) {
if (suboption == null)
continue;
createOption({
...suboption,
container: row.root
});
if (first) {
first = false;
let div = document.createElement("div");
div.style.flex = "1";
row.root.appendChild(div);
}
}
} else
createOption({ ...option, container });
}
}
/\x2f web/vview/screen-search/search-ui-desktop.js
var DesktopSearchUI = class extends Widget {
constructor(options) {
super({
...options,
template: \`
<div class=thumbnail-ui-box data-context-menu-target=off>
<!-- The images for the artist view (avatar) and related images, which shows the starting image. -->
<vv-container class=avatar-container></vv-container>
<a href=# class=image-for-suggestions>
<!-- A blank image, so we don't load anything: -->
<img src="\${helpers.other.blankImage}">
</a>
<div class=title-with-button-row-container>
<div class=title-with-button-row>
<div class="search-title title-font"></div>
</div>
</div>
<div class=button-row>
<div class="icon-button toggle-local-navigation-button popup" data-popup="Show navigation" hidden>
\${helpers.createIcon("mat:keyboard_double_arrow_left")}
</div>
<a class="icon-button disable-ui-button popup pixiv-only" data-popup="Return to Pixiv" href="#no-ppixiv">
\${helpers.createIcon("ppixiv:pixiv")}
</a>
<div class="main-search-menu-button icon-button popup pixiv-only" data-popup="Search">
\${helpers.createIcon("menu")}
</div>
<div class="refresh-search-button icon-button popup" data-popup="Refresh">
\${helpers.createIcon("refresh")}
</div>
<div class="refresh-search-from-page-button icon-button popup" data-popup="Refresh from page">
\${helpers.createIcon("restart_alt")}
</div>
<div class="expand-manga-posts icon-button popup pixiv-only">
\${helpers.createIcon("")}
</div>
<a class="slideshow icon-button popup" data-popup="Slideshow" href="#">
\${helpers.createIcon("wallpaper")}
</a>
<div class="settings-menu-box popup" data-popup="Preferences">
<div class="icon-button preferences-button">
\${helpers.createIcon("settings")}
</div>
</div>
</div>
<div class=data-source-ui style='margin: auto;'></div>
</div>
\`
});
new DropdownMenuOpener({
button: this.root.querySelector(".main-search-menu-button"),
createDropdown: ({ ...options2 }) => {
let dropdown = this.bookmarkTagsDropdown = new Widget({
...options2,
template: \`<div class="vertical-list"></div>\`
});
CreateSearchMenu(dropdown.root);
return dropdown;
}
});
this.root.querySelector(".refresh-search-from-page-button").addEventListener("click", () => this.parent.refreshSearchFromPage());
this.root.querySelector(".expand-manga-posts").addEventListener("click", (e) => {
this.parent.searchView.toggleExpandingMediaIdsByDefault();
});
this.root.querySelector(".refresh-search-button").addEventListener("click", () => this.parent.refreshSearch());
this.toggleLocalNavigationButton = this.root.querySelector(
".toggle-local-navigation-button"
);
this.toggleLocalNavigationButton.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
this.parent._localNavigationVisible = !this.parent._localNavigationVisible;
this.parent.refreshUi();
});
this.root.querySelector(".preferences-button").addEventListener("click", (e) => new SettingsDialog());
window.addEventListener(
"pp:statechange",
(e) => this.refreshRefreshSearchFromPage(),
{ signal: this.shutdownSignal }
);
this.avatarWidget = new AvatarWidget({
container: this.querySelector(".avatar-container"),
visible: false
});
this.imageForSuggestions = this.querySelector(".image-for-suggestions");
}
setDataSource(dataSource) {
if (this.dataSource == dataSource)
return;
if (this.currentDataSourceUi) {
this.currentDataSourceUi.shutdown();
this.currentDataSourceUi = null;
}
this.dataSource = dataSource;
this.avatarWidget.setUserId(null);
this.avatarWidget.visible = false;
this.imageForSuggestions.hidden = true;
if (dataSource == null)
return;
if (this.dataSource.ui) {
let dataSourceUiContainer = this.root.querySelector(".data-source-ui");
this.currentDataSourceUi = new this.dataSource.ui({
dataSource: this.dataSource,
container: dataSourceUiContainer
});
}
}
updateFromSettings = () => {
this.refreshExpandMangaPostsButton();
};
refreshUi() {
this.root.querySelector(".refresh-search-from-page-button").hidden = true;
if (this.dataSource) {
let { userId, imageUrl, imageLinkUrl } = this.dataSource.uiInfo;
this.imageForSuggestions.hidden = imageUrl == null;
this.imageForSuggestions.href = imageLinkUrl ?? "#";
let img = this.imageForSuggestions.querySelector(
".image-for-suggestions > img"
);
img.src = imageUrl ?? helpers.other.blankImage;
this.avatarWidget.visible = userId != null;
this.avatarWidget.setUserId(userId);
}
let elementTitle = this.root.querySelector(".search-title");
elementTitle.hidden = this.dataSource?.getDisplayingText == null;
if (this.dataSource?.getDisplayingText != null) {
let text = this.dataSource.getDisplayingText();
elementTitle.replaceChildren(text);
}
if (this.toggleLocalNavigationButton) {
this.toggleLocalNavigationButton.hidden = this.parent._localNavigationTree == null || !this.parent.canShowLocalNavigation;
this.toggleLocalNavigationButton.querySelector(".font-icon").innerText = this.parent._localNavigationVisible ? "keyboard_double_arrow_left" : "keyboard_double_arrow_right";
}
this.refreshSlideshowButton();
this.refreshExpandMangaPostsButton();
this.refreshRefreshSearchFromPage();
}
refreshSlideshowButton() {
let node = this.root.querySelector("A.slideshow");
node.href = ppixiv.app.slideshowURL.url;
}
refreshExpandMangaPostsButton() {
let enabled = this.parent.searchView.mediaIdsExpandedByDefault;
let button = this.root.querySelector(".expand-manga-posts");
button.dataset.popup = enabled ? "Collapse manga posts" : "Expand manga posts";
button.querySelector(".font-icon").innerText = enabled ? "close_fullscreen" : "open_in_full";
button.hidden = !this.dataSource?.allowExpandingMangaPages;
}
refreshRefreshSearchFromPage() {
if (this.dataSource == null)
return;
let startPage = this.dataSource.getStartPage(helpers.args.location);
this.root.querySelector(".refresh-search-from-page-button").dataset.popup = \`Refresh search from page \${startPage}\`;
}
};
/\x2f web/vview/screen-search/search-ui-mobile.js
var SearchUIMobile = class extends Widget {
constructor({ ...options } = {}) {
super({
...options,
template: \`
<div class=search-ui-mobile>
<div class=avatar-container style="float: right;"></div>
<div class=search-title></div>
<div class=data-source-ui style='margin: auto;'></div>
</div>
\`
});
this.avatarWidget = new AvatarWidget({
container: this.querySelector(".avatar-container"),
visible: false
});
}
setDataSource(dataSource) {
if (this._currentDataSourceUi) {
this._currentDataSourceUi.shutdown();
this._currentDataSourceUi = null;
}
this.dataSource = dataSource;
this.avatarWidget.setUserId(null);
this.avatarWidget.visible = false;
if (dataSource == null)
return;
if (this.dataSource?.ui) {
this._currentDataSourceUi = new this.dataSource.ui({
dataSource: this.dataSource,
container: this.querySelector(".data-source-ui")
});
}
}
refreshUi() {
if (this.dataSource) {
let { userId } = this.dataSource.uiInfo;
this.avatarWidget.visible = userId != null;
this.avatarWidget.setUserId(userId);
}
let elementTitle = this.querySelector(".search-title");
elementTitle.hidden = this.dataSource?.getDisplayingText == null;
if (this.dataSource?.getDisplayingText != null) {
let text = this.dataSource?.getDisplayingText();
elementTitle.replaceChildren(text);
}
}
applyVisibility() {
helpers.html.setClass(this.root, "shown", this._visible);
}
};
/\x2f web/vview/sites/illust-id-list.js
var IllustIdList = class {
constructor() {
this.mediaIdsByPage = /* @__PURE__ */ new Map();
}
getAllMediaIds() {
let allIds = [];
for (let [page, ids] of this.mediaIdsByPage)
allIds = allIds.concat(ids);
return allIds;
}
get anyPagesLoaded() {
return this.mediaIdsByPage.size != 0;
}
getLowestLoadedPage() {
return Math.min(999999, ...this.mediaIdsByPage.keys());
}
getHighestLoadedPage() {
return Math.max(0, ...this.mediaIdsByPage.keys());
}
addPage(page, mediaIds, {
allowEmpty = false
} = {}) {
for (let mediaId of mediaIds)
if (mediaId == null)
console.warn("Null illust_id added");
if (this.mediaIdsByPage.has(page)) {
console.warn("Page", page, "was already loaded");
return true;
}
let allIllusts = this.getAllMediaIds();
let idsToRemove = [];
for (let newId of mediaIds) {
if (allIllusts.indexOf(newId) != -1)
idsToRemove.push(newId);
}
if (idsToRemove.length > 0)
console.log(
"Removing duplicate illustration IDs:",
idsToRemove.join(", ")
);
mediaIds = mediaIds.slice();
for (let newId of idsToRemove) {
let idx = mediaIds.indexOf(newId);
mediaIds.splice(idx, 1);
}
if (!allowEmpty && mediaIds.length == 0)
return;
this.mediaIdsByPage.set(page, mediaIds);
}
getPageForMediaId(mediaId, { checkFirstPage = true } = {}) {
for (let [page, ids] of this.mediaIdsByPage) {
let idx = ids.indexOf(mediaId);
if (idx != -1)
return { page, idx, mediaId };
}
if (!checkFirstPage)
return {};
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
for (let [page, ids] of this.mediaIdsByPage) {
let idx = ids.indexOf(mediaId);
if (ids.indexOf(mediaId) != -1)
return { page, idx, mediaId };
}
return {};
}
getNeighboringMediaId(mediaId, next, options = {}) {
for (let i = 0; i < 100; ++i) {
mediaId = this._getNeighboringMediaIdInternal(mediaId, next, options);
if (mediaId == null)
return null;
let { type } = helpers.mediaId.parse(mediaId);
if (type == "illust" || type == "file")
return mediaId;
}
return null;
}
_getNeighboringMediaIdInternal(mediaId, next, { manga = "normal" } = {}) {
console.assert(
manga == "normal" || manga == "skip-to-first" || manga == "skip-past"
);
if (mediaId == null)
return this.getFirstId();
let id = helpers.mediaId.parse(mediaId);
if (id.type == "illust" && manga == "normal") {
if (!next && id.page > 0) {
id.page--;
return helpers.mediaId.encodeMediaId(id);
}
if (next) {
let info = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (info == null) {
console.warnning("Thumbnail info missing: ", mediaId);
} else {
let [oldIllustId, oldPage] = helpers.mediaId.toIllustIdAndPage(mediaId);
if (oldPage < info.pageCount - 1) {
id.page++;
return helpers.mediaId.encodeMediaId(id);
}
}
}
}
let { page, idx } = this.getPageForMediaId(mediaId);
if (page == null)
return null;
let newMediaId = null;
while (newMediaId == null) {
let ids = this.mediaIdsByPage.get(page);
let newIdx = idx + (next ? 1 : -1);
if (newIdx >= 0 && newIdx < ids.length) {
newMediaId = ids[newIdx];
break;
}
if (next) {
page++;
ids = this.mediaIdsByPage.get(page);
if (ids == null)
return null;
newMediaId = ids[0];
} else {
page--;
ids = this.mediaIdsByPage.get(page);
if (ids == null)
return null;
newMediaId = ids[ids.length - 1];
}
}
if (!next && manga !== "skip-to-first" && helpers.mediaId.parse(newMediaId).type === "illust") {
const info = ppixiv.mediaCache.getMediaInfoSync(newMediaId, {
full: false
});
if (info == null) {
console.warn("Thumbnail info missing: ", mediaId);
return null;
}
newMediaId = helpers.mediaId.getMediaIdForPage(
newMediaId,
info.pageCount - 1
);
}
return newMediaId;
}
getFirstId() {
if (this.mediaIdsByPage.size == 0)
return null;
let firstPage = this.getLowestLoadedPage();
return this.mediaIdsByPage.get(firstPage)[0];
}
getLastId() {
if (this.mediaIdsByPage.size == 0)
return null;
let lastPage = this.getHighestLoadedPage();
let ids = this.mediaIdsByPage.get(lastPage);
return ids[ids.length - 1];
}
isPageLoaded(page) {
return this.mediaIdsByPage.has(page);
}
};
/\x2f web/vview/sites/data-source.js
var DataSource = class extends EventTarget {
constructor({ url }) {
super();
this.url = new URL(url);
this._resetLoadedPages();
}
_resetLoadedPages() {
this.idList = new IllustIdList();
this.loadingPages = {};
this.loadedPages = {};
this.firstEmptyPage = -1;
}
async init() {
let args = new helpers.args(this.url);
this.initialPage = this.getStartPage(args);
}
get name() {
return null;
}
toString() {
return \`\${this.name}\`;
}
get allowExpandingMangaPages() {
return true;
}
get loadedAllPages() {
return this.firstEmptyPage != -1;
}
get args() {
return new helpers.args(this.url);
}
startup() {
this.active = true;
}
shutdown() {
this.active = false;
}
get searchUrl() {
return this.url;
}
get ui() {
return null;
}
loadPage(page, { cause } = {}) {
let result = this.loadedPages[page] || this.loadingPages[page];
if (result == null) {
result = this._loadPageAsync(page, { cause });
this.loadingPages[page] = result;
result.finally(() => {
delete this.loadingPages[page];
this.loadedPages[page] = result;
});
}
return result;
}
isPageLoadedOrLoading(page) {
if (this.idList.isPageLoaded(page))
return true;
if (this.loadedPages[page] || this.loadingPages[page])
return true;
return false;
}
get isAnyPageLoading() {
for (let page in this.loadingPages)
if (this.loadingPages[page])
return true;
return false;
}
canLoadPage(page) {
if (page < 1)
return false;
if (!this.idList.anyPagesLoaded)
return true;
if (this.firstEmptyPage != -1 && page >= this.firstEmptyPage)
return false;
let lowestPage = this.idList.getLowestLoadedPage();
let highestPage = this.idList.getHighestLoadedPage();
return page >= lowestPage - 1 && page <= highestPage + 1;
}
async _loadPageAsync(page, { cause }) {
if (!this.canLoadPage(page)) {
return;
}
if (this.idList.isPageLoaded(page))
return true;
console.debug(\`Load page \${page} for: \${cause}\`);
await null;
let { mediaIds, allowEmpty } = await this.loadPageInternal(page) ?? {};
if (mediaIds)
await this.addPage(page, mediaIds, { allowEmpty });
if (this.supportsStartPage && page < this.initialPage)
this.initialPage = page;
if (!this.idList.mediaIdsByPage.has(page)) {
console.log("No data on page", page);
if (this.firstEmptyPage == -1 || page < this.firstEmptyPage)
this.firstEmptyPage = page;
} else if (this.idList.mediaIdsByPage.get(page).length == 0) {
this.emptyPageLoadBackoff ??= new SafetyBackoffTimer();
console.log(
\`Load was empty, but not at the end. Delaying before loading the next page...\`
);
await this.emptyPageLoadBackoff.wait();
console.log(\`Continuing load from \${page + 1}\`);
return await this.loadPage(page + 1);
}
return true;
}
getUrlMediaId(args) {
let illustId = args.hash.get("illust_id");
if (illustId == null)
return null;
let page = this.getUrlMangaPage(args);
return helpers.mediaId.fromIllustId(illustId, page);
}
getUrlMangaPage(args) {
if (!args.hash.has("page"))
return 0;
return parseInt(args.hash.get("page")) - 1;
}
setUrlMediaId(mediaId, args) {
let [illustId, page] = helpers.mediaId.toIllustIdAndPage(mediaId);
if (this.supportsStartPage) {
let { page: originalPage } = this.idList.getPageForMediaId(illustId);
if (originalPage != null)
this.setStartPage(args, originalPage);
}
args.hash.set("illust_id", illustId);
if (page == null)
args.hash.delete("page");
else
args.hash.set("page", page + 1);
}
setStartPage(args, page) {
if (page == 1)
args.query.delete("p");
else
args.query.set("p", page);
}
getStartPage(args) {
if (!this.supportsStartPage)
return 1;
let page = args.query.get("p") || "1";
return parseInt(page) || 1;
}
get pageTitle() {
return "Pixiv";
}
setPageIcon() {
helpers.setIcon();
}
get hasNoResults() {
return this.idList.getFirstId() == null && !this.isAnyPageLoading;
}
async loadPageInternal(page) {
throw "Not implemented";
}
get estimatedItemsPerPage() {
return 48;
}
get defaultScreen() {
return "search";
}
get viewingUserId() {
return null;
}
get transient() {
return false;
}
get supportsStartPage() {
return false;
}
get autoLoadPreviousPages() {
return false;
}
getPageTextForMediaId(mediaId) {
return null;
}
async addPage(page, mediaIds, { ...options } = {}) {
let initialMediaId = this.getUrlMediaId(this.args);
if (this.allowExpandingMangaPages)
initialMediaId = helpers.mediaId.getMediaIdForPage(initialMediaId, 0);
if (page == this.initialPage && initialMediaId != null && this.idList.getPageForMediaId(initialMediaId).page == null && mediaIds.indexOf(initialMediaId) == -1) {
if (await ppixiv.mediaCache.getMediaInfo(initialMediaId, { full: false })) {
console.log(
\`Adding initial media ID \${initialMediaId} to initial page \${this.initialPage}\`
);
mediaIds = [initialMediaId, ...mediaIds];
}
}
for (let mediaId of mediaIds) {
let { type, id } = helpers.mediaId.parse(mediaId);
if (type == "user" || type == "bookmarks") {
if (ppixiv.extraCache.getQuickUserData(id) == null) {
console.error(
\`Data source returned \${mediaId} without registering user info\`,
this
);
throw new Error(\`Data source returned didn't register user info\`);
}
} else {
if (ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false }) == null) {
console.error(
\`Data source returned \${mediaId} without registering media info\`,
this
);
throw new Error(\`Data source returned didn't register media info\`);
}
}
}
this.idList.addPage(page, mediaIds, { ...options });
let e = new Event("pageadded");
e.dataSource = this;
helpers.other.defer(() => this.dispatchEvent(e));
}
callUpdateListeners() {
this.dispatchEvent(new Event("updated"));
}
get uiInfo() {
return {};
}
createAndSetButton(parent, createOptions, setupOptions) {
let button = helpers.createBoxLink({
asElement: true,
...createOptions
});
parent.appendChild(button);
this.setItem(button, setupOptions);
return button;
}
setupDropdown(button, items) {
return new DropdownMenuOpener({
button,
createDropdown: ({ ...options }) => {
let dropdown = new Widget({
...options,
template: \`<div class=vertical-list></div>\`
});
for (let { createOptions, setupOptions } of items)
this.createAndSetButton(dropdown.root, createOptions, setupOptions);
return dropdown;
}
});
}
setItem(link, { type = null, ...options } = {}) {
if (type != null) {
link = link.querySelector(\`[data-type='\${type}']\`);
if (link == null) {
console.warn("Couldn't find button with selector", type);
return;
}
}
let args = new helpers.args(this.url);
let { args: newArgs, buttonIsSelected } = this.setItemInUrl(args, options);
helpers.html.setClass(link, "selected", buttonIsSelected);
link.href = newArgs.url.toString();
}
setItemInUrl(args, {
fields = null,
defaults = null,
toggle = false,
urlFormat = null,
adjustUrl = null
} = {}) {
args.path = helpers.pixiv.getPathWithoutLanguage(args.path);
let urlParts = {};
if (urlFormat != null) {
let parts = urlFormat.split("/");
for (let idx = 0; idx < parts.length; ++idx)
urlParts["/" + parts[idx]] = "/" + idx;
}
let fieldData = {};
for (let [key, value] of Object.entries(fields)) {
let originalKey = key;
let defaultValue = null;
if (defaults && key in defaults)
defaultValue = defaults[key];
if (key.startsWith("/")) {
if (urlParts[key] == null) {
console.warn(\`URL key \${key} not specified in URL: \${args}\`);
continue;
}
key = urlParts[key];
}
fieldData[key] = {
value,
originalKey,
defaultValue
};
}
let buttonIsSelected = true;
for (let [key, { value, defaultValue }] of Object.entries(fieldData)) {
let thisValue = value ?? defaultValue;
let selectedValue = args.get(key) ?? defaultValue;
if (thisValue != selectedValue)
buttonIsSelected = false;
if (defaults != null && thisValue == defaultValue)
value = null;
args.set(key, value);
}
if (toggle && buttonIsSelected) {
for (let [key, { defaultValue }] of Object.entries(fieldData))
args.set(key, defaultValue);
}
args.set("p", null);
if (adjustUrl)
adjustUrl(args);
return { args, buttonIsSelected };
}
get showBookmarkIcons() {
return true;
}
async getOrLoadNeighboringMediaId(mediaId, next, options = {}) {
let newMediaId = this.idList.getNeighboringMediaId(mediaId, next, options);
if (newMediaId != null)
return newMediaId;
let page = mediaId != null ? this.idList.getPageForMediaId(mediaId).page : null;
if (page != null) {
page += next ? 1 : -1;
if (page < 1)
return null;
} else {
page = this.initialPage;
}
if (!this.canLoadPage(page))
return null;
console.debug("Loading the next page of results:", page);
if (this.idList.isPageLoaded(page)) {
console.error(\`Page \${page} is already loaded\`);
return null;
}
const newPageLoaded = await this.loadPage(page, {
cause: "illust navigation"
});
if (!newPageLoaded)
return null;
console.debug("Finishing navigation after data load");
return this.idList.getNeighboringMediaId(mediaId, next, options);
}
async getOrLoadNeighboringMediaIdWithLoop(fromMediaId, next, options = {}) {
let mediaId = await this.getOrLoadNeighboringMediaId(
fromMediaId,
next,
options
);
if (mediaId)
return mediaId;
if (!next && !this.loadedAllPages) {
console.log("Not looping backwards since we don't have all pages");
return null;
}
return next ? this.idList.getFirstId() : this.idList.getLastId();
}
};
var DataSourceFromPage = class extends DataSource {
constructor(url) {
super(url);
this.itemsPerPage = 1;
this.originalUrl = url;
}
get estimatedItemsPerPage() {
return this.itemsPerPage;
}
async loadPageInternal(page) {
let url = new URL(this.url);
url.searchParams.set("p", page);
console.log("Loading:", url.toString());
let doc = await helpers.pixivRequest.fetchDocument(url);
let mediaIds = this.parseDocument(doc);
if (mediaIds == null) {
console.error("No data on page");
return;
}
if (this.itemsPerPage == 1)
this.itemsPerPage = Math.max(mediaIds.length, this.itemsPerPage);
return { mediaIds };
}
parseDocument(doc) {
throw "Not implemented";
}
};
var DataSourceFakePagination = class extends DataSource {
async loadPageInternal(page) {
if (this.pages == null) {
let mediaIds2 = await this.loadAllResults();
this.pages = PaginateMediaIds(mediaIds2, this.estimatedItemsPerPage);
}
let mediaIds = this.pages[page - 1] || [];
return { mediaIds };
}
async loadAllResults() {
throw "Not implemented";
}
};
function PaginateMediaIds(mediaIds, itemsPerPage) {
let pages = [];
let page = null;
for (let mediaId of mediaIds) {
if (page == null) {
page = [];
pages.push(page);
}
page.push(mediaId);
if (page.length == itemsPerPage)
page = null;
}
return pages;
}
var TagDropdownWidget = class extends Widget {
constructor({ dataSource, ...options }) {
super({
...options,
template: \`<div class="data-source-tag-list vertical-list"></div>\`
});
this.dataSource = dataSource;
this.dataSource.addEventListener(
"updated",
() => this.refreshTags(),
this._signal
);
this.refreshTags();
}
refreshTags() {
}
};
/\x2f web/vview/sites/pixiv/data-sources/bookmarks.js
var DataSource_BookmarksBase = class extends DataSource {
get name() {
return "bookmarks";
}
get ui() {
return UI;
}
constructor(args) {
super(args);
this.bookmarkTagCounts = [];
this.totalBookmarks = -1;
}
async loadPageInternal(page) {
this.fetchBookmarkTagCounts();
let userInfoPromise = ppixiv.userCache.getUserInfo(this.viewingUserId, { full: true });
userInfoPromise.then((userInfo) => {
if (!this.active)
return;
this.userInfo = userInfo;
this.callUpdateListeners();
});
return await this.continueLoadingPageInternal(page);
}
get supportsStartPage() {
return !this.shuffle;
}
get displayingTag() {
let url = helpers.pixiv.getUrlWithoutLanguage(this.url);
let parts = url.pathname.split("/");
if (parts.length < 6)
return null;
let tag = decodeURIComponent(parts[5]);
if (tag == "未分類")
return "";
return tag;
}
async fetchBookmarkTagCounts() {
if (this.fetchedBookmarkTagCounts)
return;
this.fetchedBookmarkTagCounts = true;
if (this.viewingOwnBookmarks() && DataSource_BookmarksBase.cachedBookmarkTagCounts != null)
this.loadBookmarkTagCounts(DataSource_BookmarksBase.cachedBookmarkTagCounts);
let url = "/ajax/user/" + this.viewingUserId + "/illusts/bookmark/tags";
let result = await helpers.pixivRequest.get(url, {});
if (this.viewingOwnBookmarks())
DataSource_BookmarksBase.cachedBookmarkTagCounts = result.body;
this.loadBookmarkTagCounts(result.body);
}
loadBookmarkTagCounts(result) {
let publicBookmarks = this.viewingPublic;
let privateBookmarks = this.viewingPrivate;
let tags = {};
for (let privacy of ["public", "private"]) {
let publicTags = privacy == "public";
if (publicTags && !publicBookmarks || !publicTags && !privateBookmarks)
continue;
let tagCounts = result[privacy];
for (let tagInfo of tagCounts) {
let tag = tagInfo.tag;
if (tag == "未分類")
tag = "";
if (tags[tag] == null)
tags[tag] = 0;
tags[tag] += tagInfo.cnt;
}
}
let displayingTag = this.displayingTag;
if (displayingTag != null && this.totalBookmarks == -1) {
let count = tags[displayingTag];
if (count != null)
this.totalBookmarks = count;
}
let allTags = Object.keys(tags);
allTags.sort((lhs, rhs) => tags[lhs].count - tags[rhs].count);
if (!this.viewingOwnBookmarks()) {
allTags.splice(20);
}
allTags.sort((lhs, rhs) => lhs.toLowerCase().localeCompare(rhs.toLowerCase()));
this.bookmarkTagCounts = {};
for (let tag of allTags)
this.bookmarkTagCounts[tag] = tags[tag];
this.callUpdateListeners();
}
getBookmarkQueryParams(page, forceRest) {
let queryArgs = this.url.searchParams;
let rest = queryArgs.get("rest") || "show";
if (forceRest != null)
rest = forceRest;
let tag = this.displayingTag;
if (tag == "")
tag = "未分類";
else if (tag == null)
tag = "";
return {
tag,
offset: (page - 1) * this.estimatedItemsPerPage,
limit: this.estimatedItemsPerPage,
rest
};
}
async requestBookmarks(page, rest) {
let data = this.getBookmarkQueryParams(page, rest);
let url = \`/ajax/user/\${this.viewingUserId}/illusts/bookmarks\`;
let result = await helpers.pixivRequest.get(url, data);
if (this.viewingOwnBookmarks()) {
for (let illust of result.body.works) {
let bookmark_id = illust.bookmarkData.id;
let tags = result.body.bookmarkTags[bookmark_id] || [];
let mediaId = helpers.mediaId.fromIllustId(illust.id.toString());
ppixiv.extraCache.updateCachedBookmarkTags(mediaId, tags);
}
}
result.body.empty = result.body.works.length == 0;
result.body.works = DataSource_BookmarksBase.filterDeletedImages(result.body.works);
return result.body;
}
async continueLoadingPageInternal(page) {
throw "Not implemented";
}
get pageTitle() {
if (!this.viewingOwnBookmarks()) {
if (this.userInfo)
return this.userInfo.name + "'s Bookmarks";
else
return "Loading...";
}
return "Bookmarks";
}
getDisplayingText() {
if (!this.viewingOwnBookmarks()) {
if (this.userInfo)
return this.userInfo.name + "'s Bookmarks";
return "User's Bookmarks";
}
let publicBookmarks = this.viewingPublic;
let privateBookmarks = this.viewingPrivate;
let viewingAll = publicBookmarks && privateBookmarks;
let displaying = "";
if (this.totalBookmarks != -1)
displaying += this.totalBookmarks + " ";
displaying += viewingAll ? "Bookmark" : privateBookmarks ? "Private Bookmark" : "Public Bookmark";
if (this.totalBookmarks != 1)
displaying += "s";
let tag = this.displayingTag;
if (tag == "")
displaying += \` / untagged\`;
else if (tag != null)
displaying += \` / \${tag}\`;
return displaying;
}
get viewingPublic() {
let args = new helpers.args(this.url);
return args.query.get("rest") != "hide";
}
get viewingPrivate() {
let args = new helpers.args(this.url);
return args.query.get("rest") == "hide";
}
get uiInfo() {
return {
userId: this.viewingOwnBookmarks() ? null : this.viewingUserId
};
}
get viewingUserId() {
return helpers.strings.getPathPart(this.url, 1);
}
viewingOwnBookmarks() {
return this.viewingUserId == ppixiv.pixivInfo.userId;
}
get showBookmarkIcons() {
return !this.viewingOwnBookmarks();
}
static filterDeletedImages(images) {
let result = [];
for (let image of images) {
if (!image.isBookmarkable) {
console.log("Discarded deleted bookmark " + image.id);
continue;
}
result.push(image);
}
return result;
}
};
var Bookmarks = class extends DataSource_BookmarksBase {
get shuffle() {
let args = new helpers.args(this.url);
return args.hash.has("shuffle");
}
async continueLoadingPageInternal(page) {
let pageToLoad = page;
if (this.shuffle) {
if (this.totalShuffledBookmarks == null) {
let result2 = await this.requestBookmarks(1, null);
this.totalShuffledBookmarks = result2.total;
this.totalPages = Math.ceil(this.totalShuffledBookmarks / this.estimatedItemsPerPage);
this.shuffledPages = [];
for (let p = 1; p <= this.totalPages; ++p)
this.shuffledPages.push(p);
helpers.other.shuffleArray(this.shuffledPages);
}
if (page < this.shuffledPages.length)
pageToLoad = this.shuffledPages[page];
}
let result = await this.requestBookmarks(pageToLoad, null);
let mediaIds = [];
for (let illustData of result.works)
mediaIds.push(helpers.mediaId.fromIllustId(illustData.id));
if (this.shuffle)
helpers.other.shuffleArray(mediaIds);
await ppixiv.mediaCache.addMediaInfosPartial(result.works, "normal");
this.totalBookmarks = result.total;
return {
mediaIds,
allowEmpty: !result.empty
};
}
};
var BookmarksMerged = class extends DataSource_BookmarksBase {
get viewingPublic() {
return true;
}
get viewingPrivate() {
return true;
}
constructor(url) {
super(url);
this.maxPagePerType = [-1, -1];
this.bookmarkMediaIds = [[], []];
this.bookmarkTotals = [0, 0];
}
async continueLoadingPageInternal(page) {
let request1 = this.requestBookmarkType(page, "show");
let request2 = this.requestBookmarkType(page, "hide");
await Promise.all([request1, request2]);
let mediaIds = [];
for (let i = 0; i < 2; ++i)
if (this.bookmarkMediaIds[i] != null && this.bookmarkMediaIds[i][page] != null)
mediaIds = mediaIds.concat(this.bookmarkMediaIds[i][page]);
this.totalBookmarks = this.bookmarkTotals[0] + this.bookmarkTotals[1];
return { mediaIds };
}
async requestBookmarkType(page, rest) {
let isPrivate = rest == "hide" ? 1 : 0;
let maxPage = this.maxPagePerType[isPrivate];
if (maxPage != -1 && page > maxPage) {
console.log("page", page, "beyond", maxPage, rest);
return;
}
let result = await this.requestBookmarks(page, rest);
result.works.sort(function(lhs, rhs) {
return parseInt(rhs.bookmarkData.id) - parseInt(lhs.bookmarkData.id);
});
let mediaIds = [];
for (let illustData of result.works)
mediaIds.push(helpers.mediaId.fromIllustId(illustData.id));
await ppixiv.mediaCache.addMediaInfosPartial(result.works, "normal");
if (result.empty) {
if (this.maxPagePerType[isPrivate] == -1)
this.maxPagePerType[isPrivate] = page;
else
this.maxPagePerType[isPrivate] = Math.min(page, this.maxPagePerType[isPrivate]);
}
this.bookmarkMediaIds[isPrivate][page] = mediaIds;
this.bookmarkTotals[isPrivate] = result.total;
}
};
var UI = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, template: \`
<div class="box-button-row group">
<div class="box-button-row bookmark-type">
<!-- These are hidden if you're viewing somebody else's bookmarks. -->
\${helpers.createBoxLink({ label: "All", popup: "Show all bookmarks", dataType: "all" })}
\${helpers.createBoxLink({ label: "Public", popup: "Show public bookmarks", dataType: "public" })}
\${helpers.createBoxLink({ label: "Private", popup: "Show private bookmarks", dataType: "private" })}
</div>
\${helpers.createBoxLink({ popup: "Shuffle", icon: "shuffle", dataType: "order-shuffle" })}
\${helpers.createBoxLink({ label: "All bookmarks", popup: "Bookmark tags", icon: "ppixiv:tag", classes: ["bookmark-tag-button"] })}
</div>
\` });
this.dataSource = dataSource;
this.dataSource.addEventListener("updated", () => this.tagDropdown.setButtonPopupHighlight(), this._signal);
let publicPrivateButtonContainer = this.querySelector(".bookmark-type");
publicPrivateButtonContainer.hidden = !this.dataSource.viewingOwnBookmarks();
this.dataSource.setItem(publicPrivateButtonContainer, { type: "all", fields: { "#show-all": 1, "#shuffle": null }, defaults: { "#show-all": 1 } });
this.dataSource.setItem(this.root, { type: "public", fields: { rest: null, "#show-all": 0 }, defaults: { "#show-all": 1 } });
this.dataSource.setItem(this.root, { type: "private", fields: { rest: "hide", "#show-all": 0 }, defaults: { "#show-all": 1 } });
let args = new helpers.args(this.dataSource.url);
let showAll = args.hash.get("show-all") != "0";
let setPublic = showAll ? { rest: null, "#show-all": 0 } : {};
this.dataSource.setItem(this.root, { type: "order-shuffle", fields: { "#shuffle": 1, ...setPublic }, toggle: true, defaults: { "#shuffle": null, "#show-all": 1 } });
class BookmarkTagsDropdown extends TagDropdownWidget {
refreshTags() {
for (let tag of this.root.querySelectorAll(".tag-entry"))
tag.remove();
this.addTagLink(null);
this.addTagLink("");
let allTags = Object.keys(dataSource.bookmarkTagCounts);
allTags.sort((lhs, rhs) => lhs.toLowerCase().localeCompare(rhs.toLowerCase()));
for (let tag of allTags) {
if (tag == "")
continue;
if (dataSource.bookmarkTagCounts[tag] == 0)
continue;
this.addTagLink(tag);
}
}
addTagLink(tag) {
let label;
if (tag == null)
label = "All bookmarks";
else if (tag == "")
label = "Untagged";
else
label = tag;
let a = helpers.createBoxLink({
label,
classes: ["tag-entry"],
popup: dataSource.bookmarkTagCounts[tag],
link: "#",
asElement: true,
dataType: "bookmark-tag"
});
if (label == "All bookmarks")
a.dataset.default = 1;
if (tag == "")
tag = "未分類";
dataSource.setItem(a, {
urlFormat: "users/id/bookmarks/type/tag",
fields: { "/tag": tag }
});
this.root.appendChild(a);
}
}
;
this.tagDropdown = new DropdownMenuOpener({
button: this.querySelector(".bookmark-tag-button"),
createDropdown: ({ ...options2 }) => new BookmarkTagsDropdown({ dataSource, ...options2 })
});
}
};
/\x2f web/vview/screen-search/mobile-menu-bar.js
var MobileSearchUI = class extends Widget {
constructor(options) {
super({
...options,
template: \`
<div class=mobile-navigation-bar>
<div class="header-contents button-row">
<div class="icon-button back-button disabled">
\${helpers.createIcon("mat:arrow_back_ios_new")}
</div>
<div class="icon-button refresh-search-button">
\${helpers.createIcon("refresh")}
</div>
<div class="icon-button menu">
\${helpers.createIcon("search")}
</div>
<div class="icon-button slideshow">
\${helpers.createIcon("wallpaper")}
</div>
<div class="icon-button preferences-button">
\${helpers.createIcon("settings")}
</div>
</div>
</div>
\`
});
this.root.querySelector(".refresh-search-button").addEventListener("click", () => this.parent.refreshSearch());
this.root.querySelector(".preferences-button").addEventListener("click", (e) => new SettingsDialog());
this.root.querySelector(".slideshow").addEventListener("click", (e) => helpers.navigate(ppixiv.app.slideshowURL));
this.root.querySelector(".menu").addEventListener("click", (e) => new MobileEditSearchDialog());
this.root.querySelector(".back-button").addEventListener("click", () => {
if (ppixiv.native) {
if (this.parent.displayedMediaId == null)
return;
let parentFolderId = LocalAPI.getParentFolder(this.parent.displayedMediaId);
let args = helpers.args.location;
LocalAPI.getArgsForId(parentFolderId, args);
helpers.navigate(args);
} else if (ppixiv.phistory.permanent) {
ppixiv.phistory.back();
}
});
}
applyVisibility() {
helpers.html.setClass(this.root, "shown", this._visible);
}
refreshUi() {
let backButton = this.root.querySelector(".back-button");
let showBackButton;
if (ppixiv.native)
showBackButton = LocalAPI.getParentFolder(this.parent.displayedMediaId) != null;
else if (ppixiv.phistory.permanent)
showBackButton = ppixiv.phistory.length > 1;
helpers.html.setClass(backButton, "disabled", !showBackButton);
}
};
var MobileEditSearchDialog = class extends DialogWidget {
constructor({ ...options } = {}) {
super({
...options,
dialogClass: "edit-search-dialog",
header: "Search",
template: \`
<div class="search-selection vertical-list">
</div>
\`
});
let optionBox = this.root.querySelector(".search-selection");
CreateSearchMenu(optionBox);
this.root.addEventListener("click", (e) => {
let a = e.target.closest("A");
if (a == null)
return;
this.visible = false;
if (a.classList.contains("disable-clicks"))
e.preventDefault();
});
this.searchUrl = helpers.args.location;
this.refresh();
}
get activeRow() {
let currentDataSource = this.dataSource;
for (let button of this.root.querySelectorAll(".navigation-button")) {
let url = new URL(button.href);
let dataSourceClass = ppixiv.site.getDataSourceForUrl(url);
if (currentDataSource instanceof dataSourceClass)
return button;
if (currentDataSource instanceof DataSource_BookmarksBase && dataSourceClass.prototype instanceof DataSource_BookmarksBase)
return button;
}
throw new Error("Couldn't match data source for", currentDataSource.__proto__);
}
refresh() {
let activeRow = this.activeRow;
for (let button of this.root.querySelectorAll(".navigation-button"))
helpers.html.setClass(button, "selected", button == activeRow);
activeRow.widget.visible = true;
if (activeRow.classList.contains("artist-row")) {
let title = this.dataSource.uiInfo.mobileTitle;
if (title)
activeRow.querySelector(".label").innerText = title;
}
this._recreateUi();
}
get dataSource() {
return ppixiv.app.currentDataSource;
}
_recreateUi() {
let position = this.activeRow;
let row = position.closest(".box-link-row");
if (row)
position = row;
}
get _closeOnPopstate() {
return false;
}
};
/\x2f web/vview/actors/scroll-listener.js
var ScrollListener = class extends Actor {
constructor({
scroller,
threshold = 50,
thresholdUp = 10,
defaultValue = false,
stickyUiNode = null,
onchange = (listener) => {
},
...options
}) {
super({ ...options });
this._scroller = scroller;
this._threshold = threshold;
this._thresholdUp = thresholdUp ?? threshold;
this._onchange = onchange;
this._lastScrollY = 0;
this._defaultValue = defaultValue;
this._scrolledForwards = false;
this._stickyUiNode = stickyUiNode;
this._scroller.addEventListener("scroll", () => this._refreshAfterScroll(), this._signal);
this._recentPointerMovement = new FlingVelocity({ samplePeriod: 1 });
if (this._stickyUiNode) {
this._resizeObserver = new ResizeObserver(() => {
this._refreshAfterScroll();
});
this.shutdownSignal.addEventListener("abort", () => this._resizeObserver.disconnect());
this._resizeObserver.observe(this._stickyUiNode);
}
new ScrollDimensionsListener({
scroller,
parent: this,
onchange: () => {
this._refreshAfterScroll({ ignoreScrollHeight: true });
}
});
this.reset({ callOnchange: false });
}
reset({ resetTo = null, callOnchange = true } = {}) {
if (resetTo == null)
resetTo = this._defaultValue;
this._recentPointerMovement.reset();
this._recentPointerMovement.addSample({ y: resetTo ? this._threshold : -this._thresholdUp });
this._updateScrolledForwards({ callOnchange });
this._recentPointerMovement.reset();
}
get scrolledForwards() {
return this._scrolledForwards;
}
get _currentScrollPosition() {
return helpers.math.clamp(this._scroller.scrollTop, 0, this._scroller.scrollHeight - this._scroller.offsetHeight);
}
_refreshAfterScroll({ ignoreScrollHeight = false } = {}) {
if (ignoreScrollHeight || this._lastScrollHeight != this._scroller.scrollHeight) {
this._lastScrollHeight = this._scroller.scrollHeight;
this._lastScrollY = this._currentScrollPosition;
}
let newScrollPosition = this._currentScrollPosition;
let delta = newScrollPosition - this._lastScrollY;
this._lastScrollY = newScrollPosition;
let { distance: distance2 } = this._recentPointerMovement.getMovementInDirection("down");
if (delta > 0 != distance2 > 0)
this._recentPointerMovement.reset();
this._recentPointerMovement.addSample({ y: delta });
this._updateScrolledForwards({ callOnchange: true });
}
_updateScrolledForwards({ callOnchange }) {
let newScrollTop = this._currentScrollPosition;
let newScrollBottom = newScrollTop + this._scroller.offsetHeight;
let scrolledForwards = this._scrolledForwards;
let { distance: distance2 } = this._recentPointerMovement.getMovementInDirection("down");
if (distance2 <= -this._thresholdUp)
scrolledForwards = false;
else if (Math.abs(distance2) >= this._threshold)
scrolledForwards = true;
if (newScrollTop == 0)
scrolledForwards = false;
else if (newScrollBottom >= this._scroller.scrollHeight - 1)
scrolledForwards = true;
if (this._stickyUiNode) {
if (newScrollTop < this._stickyUiNode.offsetHeight)
scrolledForwards = false;
}
if (!this._canScroll)
scrolledForwards = this._defaultValue;
if (this._scrolledForwards == scrolledForwards)
return;
this._scrolledForwards = scrolledForwards;
if (callOnchange)
this._onchange(this);
}
get _canScroll() {
return this._scroller.scrollHeight > this._scroller.offsetHeight;
}
};
var ImmediateChildrenListener = class extends Actor {
constructor({
root,
onchange,
...options
} = {}) {
super({ ...options });
this._onchange = onchange;
this._watching = /* @__PURE__ */ new Set();
this._mutationObserver = new MutationObserver((mutations) => {
for (let mutation of mutations) {
for (let node of mutation.addedNodes)
this._nodeAdded(node, { isRoot: false });
for (let node of mutation.removedNodes)
this._nodeRemoved(node);
}
});
this.shutdownSignal.addEventListener("abort", () => this._mutationObserver.disconnect());
this._nodeAdded(root, { isRoot: true });
}
_nodeAdded(node, { isRoot: isRoot2 }) {
if (!isRoot2)
this._onchange({ node, added: true });
let isContents = getComputedStyle(node).display == "contents";
if (isRoot2 || isContents) {
console.assert(!this._watching.has(node));
this._watching.add(node);
this._mutationObserver.observe(node, { childList: true });
for (let child of node.children)
this._nodeAdded(child, { isRoot: false });
}
}
_nodeRemoved(node) {
this._onchange({ node, added: false });
let isContents = getComputedStyle(node).display == "contents";
if (isRoot || isContents) {
console.assert(this._watching.has(node));
this._watching.remove(node);
this._mutationObserver.unobserve(node, { childList: true });
for (let child of node.children) {
this._nodeRemoved(child);
}
}
}
};
var ScrollDimensionsListener = class extends Actor {
constructor({
scroller,
onchange = (listener) => {
},
...options
} = {}) {
super({ ...options });
this.onchange = onchange;
this._resizeObserver = new ResizeObserver(() => {
this.onchange(this);
});
this.shutdownSignal.addEventListener("abort", () => this._resizeObserver.disconnect());
this._childrenListener = new ImmediateChildrenListener({
parent: this,
root: scroller,
onchange: ({ node, added }) => {
if (added)
this._resizeObserver.observe(node);
else
this._resizeObserver.unobserve(node);
}
});
}
};
/\x2f web/vview/widgets/folder-tree.js
var TreeWidget = class extends Widget {
constructor({
addRoot = true,
...options
}) {
super({ ...options, template: \`
<div class=tree>
<div class=items>
</div>
</div>
\` });
this._labelPopup = this.createTemplate({ html: \`
<div class=tree-popup>
<div class=label></div>
</div>
\` });
this._thumbPopup = this.createTemplate({ html: \`
<div class=thumb-popup>
<img class=img></div>
</div>
\` });
this.items = this.root.querySelector(".items");
ppixiv.mediaCache.addEventListener("mediamodified", this._mediaModified, { signal: this.shutdownSignal });
if (addRoot) {
let rootItem = new TreeWidgetItem({
parent: this,
label: "root",
rootItem: true
});
this.setRootItem(rootItem);
}
}
_mediaModified = (e) => {
if (this.rootItem == null)
return;
for (let node of Object.values(this.rootItem.nodes)) {
if (node.illustChanged)
node.illustChanged(e.mediaId);
}
};
setRootItem(rootItem) {
if (this.rootItem == rootItem)
return;
if (this.rootItem) {
this.rootItem.root.remove();
this.rootItem = null;
}
this.rootItem = rootItem;
if (rootItem.root.parentNode != this.items) {
console.assert(rootItem.parentNode == null);
this.items.appendChild(rootItem.root);
}
rootItem.expanded = "user";
}
setSelectedItem(item) {
if (this.selectedItem == item)
return;
this.selectedItem = item;
for (let node of this.root.querySelectorAll(".tree-item.selected")) {
node.classList.remove("selected");
node.widget._collapseAutoExpanded({ untilAncestorOf: item?.parent });
}
if (item != null) {
item.root.classList.add("selected");
let scrollContainer = this.root;
let label = item.root.querySelector(".label");
let oldScrollLeft = scrollContainer.scrollLeft;
label.scrollIntoView({ block: "nearest" });
scrollContainer.scrollLeft = oldScrollLeft;
}
}
setHover(item) {
let img = this._thumbPopup.querySelector("img");
if (item == null) {
img.src = helpers.other.blankImage;
this._labelPopup.remove();
this._thumbPopup.remove();
return;
}
let label = item.root.querySelector(".label");
let { top, left, bottom, height } = label.getBoundingClientRect();
if (item.path) {
let { right } = this.root.getBoundingClientRect();
this._thumbPopup.style.left = \`\${right}px\`;
let labelCenter = top + height / 2;
let belowMiddle = labelCenter > window.innerHeight / 2;
if (belowMiddle) {
this._thumbPopup.style.top = \`\${top - 20}px\`;
img.style.objectPosition = "left bottom";
this._thumbPopup.style.transform = "translate(0, -100%)";
} else {
this._thumbPopup.style.top = \`\${bottom + 20}px\`;
img.style.objectPosition = "left top";
this._thumbPopup.style.transform = "";
}
img.hidden = item.isRootItem;
img.crossOriginMode = "use-credentials";
if (!item.isRootItem) {
let url = LocalAPI.localUrl;
url.pathname = "tree-thumb/" + item.path;
img.src = url;
img.addEventListener("img", (e) => {
console.log("error");
img.hidden = true;
});
}
document.body.appendChild(this._thumbPopup);
}
{
this._labelPopup.style.left = \`\${left}px\`;
this._labelPopup.style.top = \`\${top}px\`;
this._labelPopup.style.padding = getComputedStyle(label).padding;
this._labelPopup.querySelector(".label").innerText = item.label;
document.body.appendChild(this._labelPopup);
}
}
};
var TreeWidgetItem = class extends Widget {
constructor({
parent,
label,
rootItem = false,
pending = false,
expandable = false,
...options
} = {}) {
if (!rootItem && parent instanceof TreeWidget)
parent = parent.rootItem;
super({
...options,
container: parent.items,
template: \`
<div class=tree-item data-context-menu-target>
<div class=self tabindex=1>
<div class=expander data-mode="loading">
<span class="expander-button expand">▶</span>
<span class="expander-button loading">⌛</span>
<span class="expander-button none"></span>
</div>
<div class="button-bookmark public enabled bookmarked" hidden>
<ppixiv-inline src="resources/heart-icon.svg"></ppixiv-inline>
</div>
<div class=label></div>
</div>
<div class=items></div>
</div>
\`
});
if (rootItem) {
this.root.querySelector(".self").hidden = true;
this.root.classList.add("root-item");
}
helpers.html.setClass(this.root, "top", !rootItem && parent.rootItem);
helpers.html.setClass(this.root, "child", !rootItem && !parent.rootItem);
this.items = this.root.querySelector(".items");
this.expander = this.root.querySelector(".expander");
this.isRootItem = rootItem;
this._expandable = expandable;
this._expanded = false;
this._pending = pending;
this._label = label;
this.rootNode = rootItem ? this : this.parent.rootNode;
this.tree = rootItem ? this.parent : this.parent.tree;
this.expander.addEventListener("click", (e) => {
this.expanded = this.expanded ? false : "user";
});
let labelElement = this.root.querySelector(".label");
labelElement.addEventListener("dblclick", this.ondblclick);
labelElement.addEventListener("mousedown", (e) => {
if (e.button != 0)
return;
e.preventDefault();
e.stopImmediatePropagation();
this.select({ user: true });
this.onclick();
}, { capture: true });
labelElement.addEventListener("mouseover", (e) => {
this.tree.setHover(this);
}, {
capture: false
});
labelElement.addEventListener("mouseout", (e) => {
this.tree.setHover(null);
}, {
capture: false
});
this._refreshExpandMode();
if (this.parent instanceof TreeWidgetItem) {
this.parent._refreshExpandMode();
}
this.refresh();
}
get label() {
return this._label;
}
refresh() {
let label = this.root.querySelector(".label");
label.innerText = this.label;
}
async onexpand() {
return true;
}
onclick() {
}
set expanded(value) {
if (this._expanded == value)
return;
if (value == "auto" && this._expanded == "user")
return;
if (!value && this.isRootItem)
return;
this._expanded = value;
if (this._expanded)
this._loadContents();
this._refreshExpandMode();
}
async _loadContents() {
if (!this._pending)
return;
if (this._loadPromise != null) {
try {
await this._loadPromise;
} catch (e) {
}
return;
}
this._loadPromise = this.onexpand();
this._loadPromise.finally(() => {
this.pending = false;
this._loadPromise = null;
});
try {
if (await this._loadPromise)
return;
} catch (e) {
console.log("Error expanding", this, e);
}
this._pending = true;
this._expanded = false;
this._refreshExpandMode();
}
set expandable(value) {
if (this._expandable == value)
return;
this._expandable = value;
this._refreshExpandMode();
}
set pending(value) {
if (this._pending == value)
return;
this._pending = value;
this._refreshExpandMode();
}
get expanded() {
return this._expanded;
}
get expandable() {
return this._expandable;
}
get pending() {
return this._pending;
}
get childNodes() {
let result = [];
for (let child = this.items.firstElementChild; child != null; child = child.nextElementSibling)
if (child.widget)
result.push(child.widget);
return result;
}
get displayedExpandMode() {
if (!this._pending && this.items.firstElementChild == null)
return "none";
if (this.expanded)
return this._pending ? "loading" : "expanded";
return "expandable";
}
_refreshExpandMode() {
this.expander.dataset.mode = this.displayedExpandMode;
this.expander.dataset.pending = this._pending;
this.items.hidden = !this._expanded || this._pending;
helpers.html.setClass(this.root, "allow-content-visibility", this.displayedExpandMode != "expanded");
}
select({ user = false } = {}) {
this.tree.setSelectedItem(this);
if (user)
this._commitUserExpanded();
}
_commitUserExpanded() {
let widget = this;
while (widget != null && !widget.isRootItem) {
if (widget.expanded)
widget.expanded = "user";
widget = widget.parent;
}
}
_collapseAutoExpanded({ untilAncestorOf } = {}) {
let stopNodes = /* @__PURE__ */ new Set();
for (let node = untilAncestorOf; node != null; node = node.parent)
stopNodes.add(node);
let widget = this;
while (widget != null && !widget.isRootItem) {
if (stopNodes.has(widget))
break;
if (widget.expanded == "auto")
widget.expanded = false;
widget = widget.parent;
}
}
focus() {
this.root.querySelector(".self").focus();
}
remove() {
if (this.parent == null)
return;
this.parent.items.remove(this.root);
this.parent._refreshExpandMode();
this.parent = null;
}
ondblclick = async (e) => {
e.preventDefault();
e.stopImmediatePropagation();
console.log("ondblclick");
this.expanded = this.expanded ? false : "user";
console.log("loading on dblclick");
await this._loadContents();
if (this.childNodes.length != 0)
return;
let dataSource = ppixiv.app.currentDataSource;
console.log("data source for double click:", dataSource);
if (!dataSource.idList.isPageLoaded(1))
await dataSource.loadPage(1);
let mediaIds = dataSource.idList.mediaIdsByPage.get(1);
console.log("files for double click:", mediaIds?.length);
if (mediaIds != null)
ppixiv.app.showMediaId(mediaIds[0], { addToHistory: true, source: "dblclick" });
};
};
var LocalNavigationWidgetItem = class extends TreeWidgetItem {
constructor({ path, ...options } = {}) {
super({
...options,
expandable: true,
pending: true
});
this.options = options;
this.path = path;
let { id } = helpers.mediaId.parse(this.path);
let isLibrary = id.indexOf("/", 1) == -1;
if (!isLibrary)
this.root.dataset.mediaId = this.path;
if (options.rootItem) {
this.nodes = {};
this.nodes[path] = this;
}
}
illustChanged(mediaId) {
if (mediaId == this.path)
this.refresh();
}
refresh() {
super.refresh();
let info = ppixiv.mediaCache.getMediaInfoSync(this.path, { full: false });
let bookmarked = info?.bookmarkData != null;
this.root.querySelector(".button-bookmark").hidden = !bookmarked;
}
async onexpand() {
return await this.load();
}
onclick() {
this.tree.showItem(this.path);
}
load() {
if (this.loaded)
return Promise.resolve(true);
if (this._loadPromise)
return this._loadPromise;
this._loadPromise = this.loadInner();
this._loadPromise.finally(() => {
this._loadPromise = null;
});
return this._loadPromise;
}
async loadInner(item) {
if (this.loaded)
return true;
this.loaded = true;
let result = await ppixiv.mediaCache.localSearch(this.path, {
id: this.path,
directories_only: true
});
if (!result.success) {
this.loaded = false;
return false;
}
for (let dir of result.results) {
let { type } = helpers.mediaId.parse(dir.mediaId);
if (type != "folder")
continue;
let child = new LocalNavigationWidgetItem({
parent: this,
label: dir.illustTitle,
path: dir.mediaId
});
this.rootNode.nodes[child.path] = child;
if (this.path == "folder:/")
child.expanded = "user";
}
return true;
}
};
var LocalNavigationTreeWidget = class extends TreeWidget {
constructor({ ...options } = {}) {
super({
...options,
addRoot: false
});
this._loadPathRunner = new GuardedRunner(this._signal);
this.roots = {};
window.addEventListener("pp:popstate", (e) => {
this.setRootFromUrl();
this.refreshSelection();
});
this.setRootFromUrl();
this.refreshSelection({ user: true });
}
setRootFromUrl() {
let args = helpers.args.location;
if (args.path != LocalAPI.path)
return;
if (this._root == null) {
this._root = new LocalNavigationWidgetItem({
parent: this,
label: "/",
rootItem: true,
path: "folder:/"
});
}
this.setRootItem(this._root);
}
setRootItem(rootItem) {
super.setRootItem(rootItem);
rootItem.load();
}
async refreshSelection({ user = false } = {}) {
if (this.rootItem == null)
return;
let args = helpers.args.location;
if (args.path != LocalAPI.path) {
this.setSelectedItem(null);
return;
}
let node = await this._loadPathRunner.call(this.loadPath.bind(this), { args, user });
if (node) {
node.select({ user });
return;
}
}
async loadPath({ args, user = false, signal } = {}) {
if (this.rootItem == null)
return;
await this.rootItem.load();
signal.throwIfAborted();
let mediaId = LocalAPI.getLocalIdFromArgs(args, { getFolder: true });
let { id } = helpers.mediaId.parse(mediaId);
let parts = id.split("/");
parts.splice(parts.length - 1, 1);
let currentPath = "";
let node = null;
for (let part of parts) {
if (currentPath == "")
currentPath = "folder:/";
else if (currentPath != "folder:/")
currentPath += "/";
currentPath += part;
node = this.rootItem.nodes[currentPath];
if (node == null) {
continue;
}
node.expanded = user ? "user" : "auto";
if (node._loadPromise)
await node._loadPromise;
signal.throwIfAborted();
}
return this.rootItem.nodes[mediaId];
}
showItem(mediaId) {
let args = new helpers.args(ppixiv.plocation);
LocalAPI.getArgsForId(mediaId, args);
helpers.navigate(args);
this.setHover(null);
}
};
/\x2f web/vview/actors/stop-animation-after.js
var StopAnimationAfter = class {
constructor(animation, delay, duration, vertical) {
this.animation = animation;
this.delay = delay;
this.duration = duration;
this.vertical = vertical;
this.abort = new AbortController();
this.run();
}
async run() {
let last_activity_at = Date.now() / 1e3;
let onmove = (e) => {
last_activity_at = Date.now() / 1e3;
};
window.addEventListener("mousemove", onmove, {
passive: true
});
try {
const duration = this.vertical ? 1e-3 : 0.3;
let stop_at_animation_time = null;
while (1) {
let success = await helpers.other.vsync({ signal: this.abort.signal });
if (!success)
break;
let now = Date.now() / 1e3;
let stopping = now >= last_activity_at + this.delay;
if (!stopping) {
stop_at_animation_time = null;
this.animation.playbackRate = 1;
continue;
}
if (stop_at_animation_time == null) {
stop_at_animation_time = this.animation.currentTime / 1e3 + 1e-4;
stop_at_animation_time = Math.ceil(stop_at_animation_time / 4) * 4;
}
let animation_time = this.animation.currentTime / 1e3;
let animation_time_left = stop_at_animation_time - animation_time;
if (animation_time_left > duration) {
this.animation.playbackRate = 1;
continue;
}
if (animation_time_left <= 1e-3) {
this.animation.playbackRate = 0;
continue;
}
this.animation.playbackRate = animation_time_left / duration;
}
} finally {
window.removeEventListener("mousemove", onmove);
}
}
shutdown() {
this.abort.abort();
this.animation.playbackRate = 1;
}
};
/\x2f web/vview/screen-search/search-view.js
var ThumbnailGrid = class {
constructor({ container }) {
this.container = container;
this.rows = [];
this.sizingStyle = null;
}
clear() {
for (let row of this.rows)
row.remove();
this.rows = [];
}
addThumbToRow(node, { atEnd }) {
let row = this.getRow({ atEnd });
row.insertAdjacentElement(atEnd ? "beforeend" : "afterbegin", node);
this.alignRow(row);
let resultWidth = (row.children.length - 1) * this.sizingStyle.padding;
for (let thumb of row.children)
resultWidth += thumb.currentWidth;
resultWidth = Math.round(resultWidth);
if (resultWidth <= this.sizingStyle.containerWidth)
return row;
if (row.children.length == 1) {
console.error("Single thumbnail didn't scale to fit:", row);
return;
}
node.remove();
this.alignRow(row);
let newRow = this.createRow({ atEnd });
newRow.insertAdjacentElement(atEnd ? "beforeend" : "afterbegin", node);
return newRow;
}
getRow({ atEnd = true } = {}) {
let row = atEnd ? this.rows[this.rows.length - 1] : this.rows[0];
if (row)
return row;
else
return this.createRow({ atEnd });
}
createRow({ atEnd = true } = {}) {
let row = document.realCreateElement("div");
row.className = "row";
if (atEnd) {
this.container.insertAdjacentElement("beforeend", row);
this.rows.push(row);
} else {
this.container.insertAdjacentElement("afterbegin", row);
this.rows.splice(0, 0, row);
}
return row;
}
getAverageHeightOfRow(row) {
if (row.children.length == 0)
return 0;
let totalHeight = 0;
for (let thumb of row.children)
totalHeight += thumb.origHeight;
return totalHeight / row.children.length;
}
alignRow(row) {
for (let thumb of row.children) {
thumb.currentWidth = thumb.origWidth;
thumb.currentHeight = thumb.origHeight;
}
if (this.sizingStyle.thumbnailStyle != "aspect") {
this.applySizes(row);
return;
}
let averageHeight = this.getAverageHeightOfRow(row);
for (let thumb of row.children) {
let ratio = averageHeight / thumb.currentHeight;
thumb.currentHeight *= ratio;
thumb.currentWidth *= ratio;
}
let rowWidth = 0;
for (let thumb of row.children)
rowWidth += thumb.currentWidth;
let containerWidth = this.sizingStyle.containerWidth - (row.children.length - 1) * this.sizingStyle.padding;
let scaleX = containerWidth / rowWidth;
let maxAllowedHeight = this.sizingStyle.thumbHeight * 2;
scaleX = Math.min(scaleX, maxAllowedHeight / averageHeight);
if (row.children.length > 1)
scaleX = Math.max(scaleX, 1);
let scaleY = scaleX;
for (let thumb of row.children) {
thumb.currentWidth *= scaleX;
thumb.currentHeight *= scaleY;
}
this.applySizes(row);
}
applySizes(row) {
let rowHeight = row.children[0]?.currentHeight ?? 128;
row.style.setProperty("--row-height", \`\${rowHeight}px\`);
for (let thumb of row.children) {
thumb.style.setProperty("--thumb-width", \`\${thumb.currentWidth}px\`);
thumb.style.setProperty("--thumb-height", \`\${thumb.currentHeight}px\`);
}
}
};
var SearchView = class extends Widget {
constructor({ ...options }) {
super({
...options,
template: \`
<div class=search-view data-context-menu-target>
<div class=artist-header hidden>
<div class=shape>
<img class=bg>
</div>
</div>
<div class=no-results hidden>
<div class=message>No results</div>
</div>
<div class=thumbnails></div>
</div>
\`
});
this.scrollContainer = this.root.closest(".scroll-container");
this.thumbnailBox = this.root.querySelector(".thumbnails");
this._setDataSourceRunner = new GuardedRunner(this._signal);
this._loadPageRunner = new GuardedRunner(this._signal);
this.grid = new ThumbnailGrid({
container: this.thumbnailBox
});
this.artistHeader = this.querySelector(".artist-header");
this.thumbs = {};
this.expandedMediaIds = /* @__PURE__ */ new Map();
this._mediaIdExpandedCache = null;
let resizeObserver = new ResizeObserver(
() => this.refreshImages({ cause: "resize" })
);
resizeObserver.observe(this.scrollContainer);
resizeObserver.observe(this.thumbnailBox);
new ResizeObserver(() => {
let args = helpers.args.location;
if (args.state.scroll)
this.restoreScrollPosition(args.state.scroll?.scrollPosition);
}).observe(this.scrollContainer);
ppixiv.mediaCache.addEventListener(
"mediamodified",
(e) => this.refreshThumbnail(e.mediaId),
this._signal
);
this.root.addEventListener(
"load",
(e) => {
if (e.target.classList.contains("thumb"))
this.thumbImageLoadFinished(e.target.closest(".thumbnail-box"), {
cause: "onload"
});
},
{ capture: true }
);
this.scrollContainer.addEventListener(
"scroll",
(e) => this.scheduleStoreScrollPosition(),
{ passive: true }
);
this.thumbnailBox.addEventListener("click", (e) => this.thumbnailClick(e));
this.thumbnailBox.addEventListener(
"mousedown",
async (e) => {
if (e.button != 0)
return;
let a = e.target.closest("a.thumbnail-link");
if (a == null)
return;
if (a.dataset.mediaId == null)
return;
let { type } = helpers.mediaId.parse(a.dataset.mediaId);
if (type != "illust")
return;
await ppixiv.mediaCache.getMediaInfo(a.dataset.mediaId);
},
{ capture: true }
);
new PointerListener({
element: this.thumbnailBox,
buttonMask: 1,
callback: (e) => {
if (!e.pressed)
return;
let a = e.target.closest("A");
if (a == null)
return;
if (!ppixiv.settings.get("quick_view"))
return;
if (e.pointerType != "mouse")
return;
let { mediaId } = ppixiv.app.getMediaIdAtElement(e.target);
if (mediaId == null)
return;
e.preventDefault();
ppixiv.app.showMediaId(mediaId, { addToHistory: true });
}
});
this.intersectionObservers = [];
this.intersectionObservers.push(
new IntersectionObserver(
(entries) => {
for (let entry of entries)
helpers.html.setDataSet(
entry.target.dataset,
"nearby",
entry.isIntersecting
);
this.refreshImages({ cause: "nearby-observer" });
this.loadDataSourcePage();
},
{
root: this.scrollContainer,
rootMargin: ppixiv.mobile ? "400%" : "150%"
}
)
);
ppixiv.settings.addEventListener(
"thumbnail-size",
() => this.updateFromSettings(),
this._signal
);
ppixiv.settings.addEventListener(
"disable_thumbnail_zooming",
() => this.updateFromSettings(),
this._signal
);
ppixiv.settings.addEventListener(
"disable_thumbnail_panning",
() => this.updateFromSettings(),
this._signal
);
ppixiv.settings.addEventListener(
"expand_manga_thumbnails",
() => this.updateFromSettings(),
this._signal
);
ppixiv.settings.addEventListener(
"thumbnail_style",
() => this.updateFromSettings(),
this._signal
);
ppixiv.settings.addEventListener(
"pixiv_cdn",
() => this.updateFromSettings(),
this._signal
);
ppixiv.settings.addEventListener(
"show_ai_icon",
() => this.refreshAllBookmarkIcons(),
this._signal
);
ppixiv.muting.addEventListener(
"mutes-changed",
() => this.refreshAfterMuteChange(),
this._signal
);
this.updateFromSettings();
}
updateFromSettings() {
this.refreshExpandedThumbAll();
this.loadExpandedMediaIds();
this.refreshImages({ cause: "settings" });
let disableThumbnailZooming = ppixiv.settings.get("disable_thumbnail_zooming") || ppixiv.mobile;
if (ppixiv.settings.get("thumbnail_style") == "aspect")
disableThumbnailZooming = true;
helpers.html.setClass(
document.body,
"disable-thumbnail-zooming",
disableThumbnailZooming
);
}
getThumbnailForMediaId(mediaId, { fallbackOnPage1 = false } = {}) {
if (this.thumbs[mediaId] != null)
return this.thumbs[mediaId];
if (fallbackOnPage1) {
let page1MediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
if (page1MediaId != mediaId && this.thumbs[page1MediaId] != null)
return this.thumbs[page1MediaId];
}
return null;
}
getFirstFullyOnscreenThumb() {
let screenTop = this.scrollContainer.scrollTop + this.scrollContainer.offsetHeight / 4;
let centerRow = null;
let bestDistance = 999999;
for (let row of this.grid.rows) {
let rowTop = row.offsetTop;
let distance2 = Math.abs(rowTop - screenTop);
if (distance2 < Math.abs(bestDistance)) {
bestDistance = distance2;
centerRow = row;
}
}
if (centerRow)
return centerRow.firstElementChild;
return null;
}
setDataSource(dataSource, { targetMediaId } = {}) {
return this._setDataSourceRunner.call(this._setDataSource.bind(this), {
dataSource,
targetMediaId
});
}
async _setDataSource({ dataSource, targetMediaId, signal } = {}) {
if (dataSource != this.dataSource) {
if (this.dataSource != null)
this.dataSource.removeEventListener("updated", this.dataSourceUpdated);
this._clearThumbs();
this._mediaIdExpandedCache = null;
this.dataSource = dataSource;
this.dataSource.addEventListener("updated", this.dataSourceUpdated);
this.refreshHeader();
}
this.loadExpandedMediaIds();
await this.loadDataSourcePage({ cause: "initialization" });
signal.throwIfAborted();
let args = helpers.args.location;
let scrollMediaId = args.state.scroll?.scrollPosition?.mediaId;
this.refreshImages({
cause: "initial",
targetMediaId: targetMediaId ?? scrollMediaId
});
if (targetMediaId != null)
this.scrollToMediaId(targetMediaId);
else if (!this.restoreScrollPosition(args.state.scroll?.scrollPosition))
this.scrollContainer.scrollTop = 0;
}
loadDataSourcePage({ cause = "thumbnails" } = {}) {
if (this._loadPageRunner.isRunning)
return this._loadPageRunner.promise;
return this._loadPageRunner.call(this._loadDataSourcePageInner.bind(this), {
cause
});
}
async _loadDataSourcePageInner({ cause = "thumbnails", signal } = {}) {
let loadPage = this._dataSourcePageToLoad;
if (loadPage == null)
return;
let noResults = this.root.querySelector(".no-results");
noResults.hidden = true;
await this.dataSource.loadPage(loadPage, { cause });
if (!this._setDataSourceRunner.isRunning)
this.refreshImages({ cause: "data-source-updated" });
signal.throwIfAborted();
if (this.dataSource?.hasNoResults)
noResults.hidden = false;
(async () => {
await helpers.other.sleep(100);
this.loadDataSourcePage();
})();
}
get _dataSourcePageToLoad() {
if (this.dataSource && !this.dataSource.isPageLoadedOrLoading(this.dataSource.initialPage))
return this.dataSource.initialPage;
let thumbs = this.getLoadedThumbs();
if (thumbs.length == 0)
return null;
let lastThumb = thumbs[thumbs.length - 1];
if (lastThumb.dataset.nearby) {
let loadPage = parseInt(lastThumb.dataset.searchPage) + 1;
if (this.dataSource.canLoadPage(loadPage) && !this.dataSource.isPageLoadedOrLoading(loadPage))
return loadPage;
}
let firstThumb = thumbs[0];
if (firstThumb.dataset.nearby) {
let loadPage = parseInt(firstThumb.dataset.searchPage) - 1;
if (!this.dataSource.isPageLoadedOrLoading(loadPage))
return loadPage;
}
return null;
}
async activate() {
this._active = true;
let focus = document.querySelector(":focus");
if (focus == null)
this.scrollContainer.focus();
await this._setDataSourceRunner.promise;
}
deactivate() {
if (!this._active)
return;
this._active = false;
this.stopPulsingThumbnail();
}
scheduleStoreScrollPosition() {
if (this.scrollPositionTimer != -1) {
realClearTimeout(this.scrollPositionTimer);
this.scrollPositionTimer = -1;
}
this.scrollPositionTimer = realSetTimeout(() => {
this.storeScrollPosition();
}, 100);
}
storeScrollPosition() {
if (this._setDataSourceRunner.isRunning)
return;
let args = helpers.args.location;
if (this.dataSource?.supportsStartPage) {
let firstThumb = this.getFirstFullyOnscreenThumb();
if (firstThumb?.dataset?.searchPage != null)
this.dataSource.setStartPage(args, firstThumb.dataset.searchPage);
}
args.state.scroll = {
scrollPosition: this.saveScrollPosition()
};
helpers.navigate(args, {
addToHistory: false,
cause: "viewing-page",
sendPopstate: false
});
}
dataSourceUpdated = () => {
this.refreshHeader();
};
getDataSourceMediaIds() {
let allMediaIds = [];
let mediaIdPages = {};
if (this.dataSource == null)
return { allMediaIds, mediaIdPages };
let idList = this.dataSource.idList;
let minPage = idList.getLowestLoadedPage();
let maxPage = idList.getHighestLoadedPage();
for (let page = minPage; page <= maxPage; ++page) {
let mediaIdsOnPage = idList.mediaIdsByPage.get(page);
console.assert(mediaIdsOnPage != null);
for (let mediaId of mediaIdsOnPage) {
let mediaIdsOnPage2 = this._getExpandedPages(mediaId);
if (mediaIdsOnPage2 != null) {
for (let pageMediaId of mediaIdsOnPage2) {
allMediaIds.push(pageMediaId);
mediaIdPages[pageMediaId] = page;
}
continue;
}
allMediaIds.push(mediaId);
mediaIdPages[mediaId] = page;
}
}
if (allMediaIds.length != new Set(allMediaIds).size)
throw Error("Duplicate media IDs");
return { allMediaIds, mediaIdPages };
}
_getExpandedPages(mediaId) {
if (!this.isMediaIdExpanded(mediaId))
return null;
let info = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (info == null || info.pageCount <= 1)
return null;
let results = [];
let { type, id } = helpers.mediaId.parse(mediaId);
for (let mangaPage = 0; mangaPage < info.pageCount; ++mangaPage) {
let pageMediaId = helpers.mediaId.encodeMediaId({
type,
id,
page: mangaPage
});
results.push(pageMediaId);
}
return results;
}
getMediaIdsToDisplay({ allMediaIds, targetMediaId }) {
if (allMediaIds.length == 0)
return { startIdx: 0, endIdx: 0 };
let startIdx = 0, endIdx = 0;
let targetMediaIdIdx = allMediaIds.indexOf(targetMediaId);
if (targetMediaId && this.thumbs[targetMediaId] == null && targetMediaIdIdx != -1) {
startIdx = targetMediaIdIdx;
endIdx = targetMediaIdIdx;
} else {
startIdx = 999999;
endIdx = 0;
let [firstLoadedMediaId, lastLoadedMediaId] = this.getLoadedMediaIds();
let firstLoadedMediaIdIdx = allMediaIds.indexOf(firstLoadedMediaId);
let lastLoadedMediaIdIdx = allMediaIds.indexOf(lastLoadedMediaId);
if (firstLoadedMediaIdIdx != -1 && lastLoadedMediaIdIdx != -1) {
startIdx = firstLoadedMediaIdIdx;
endIdx = lastLoadedMediaIdIdx;
} else {
startIdx = 0;
endIdx = 0;
}
let chunkSizeForwards = 25;
let [firstNearbyMediaId, lastNearbyMediaId] = this.getNearbyMediaIds();
let lastNearbyMediaIdIdx = allMediaIds.indexOf(lastNearbyMediaId);
if (lastNearbyMediaIdIdx != -1 && lastNearbyMediaIdIdx == lastLoadedMediaIdIdx)
endIdx += chunkSizeForwards;
let chunkSizeBackwards = ppixiv.ios ? 100 : 25;
let firstNearbyMediaIdIdx = allMediaIds.indexOf(firstNearbyMediaId);
if (firstNearbyMediaIdIdx != -1 && firstNearbyMediaIdIdx == firstLoadedMediaIdIdx)
startIdx -= chunkSizeBackwards;
}
startIdx = Math.max(startIdx, 0);
endIdx = Math.min(endIdx, allMediaIds.length - 1);
endIdx = Math.max(startIdx, endIdx);
let { desiredPixels, containerWidth } = this.sizingStyle;
let viewPixels = containerWidth * this.scrollContainer.offsetHeight;
viewPixels *= 2;
while (1) {
let totalThumbs = endIdx - startIdx + 1;
if (totalThumbs >= allMediaIds.length)
break;
let totalPixels = totalThumbs * desiredPixels;
if (totalPixels >= viewPixels)
break;
if (startIdx > 0)
startIdx--;
if (endIdx + 1 < allMediaIds.length)
endIdx++;
}
return { startIdx, endIdx };
}
getNearbyMediaIds({ all = false } = {}) {
let mediaIds = [];
for (let [mediaId, element] of Object.entries(this.thumbs)) {
if (element.dataset.nearby)
mediaIds.push(mediaId);
}
if (all)
return mediaIds;
else
return [mediaIds[0], mediaIds[mediaIds.length - 1]];
}
getLoadedMediaIds() {
let mediaIds = Object.keys(this.thumbs);
let firstLoadedMediaId = mediaIds[0];
let lastLoadedMediaId = mediaIds[mediaIds.length - 1];
return [firstLoadedMediaId, lastLoadedMediaId];
}
refreshImages({
targetMediaId = null,
purge = false,
cause
} = {}) {
if (this.dataSource == null)
return;
let oldSizingStyle = this.sizingStyle;
this.sizingStyle = this.makeThumbnailSizingStyle();
this.grid.sizingStyle = this.sizingStyle;
let savedScroll = this.saveScrollPosition();
let { padding, containerWidth } = this.sizingStyle;
this.root.style.setProperty("--thumb-padding", \`\${padding}px\`);
this.root.style.setProperty("--container-width", \`\${containerWidth}px\`);
this.root.style.setProperty(
"--thumb-width",
\`\${this.sizingStyle.thumbWidth}px\`
);
this.root.style.setProperty(
"--row-height",
\`\${this.sizingStyle.thumbHeight}px\`
);
if (oldSizingStyle && JSON.stringify(oldSizingStyle) != JSON.stringify(this.sizingStyle))
purge = true;
if (purge) {
targetMediaId ??= savedScroll?.mediaId;
this._clearThumbs();
}
let { allMediaIds, mediaIdPages } = this.getDataSourceMediaIds();
if (targetMediaId != null && allMediaIds.indexOf(targetMediaId) == -1)
targetMediaId = helpers.mediaId.getMediaIdFirstPage(targetMediaId);
let { startIdx, endIdx } = this.getMediaIdsToDisplay({
allMediaIds,
targetMediaId
});
let mediaIds = allMediaIds.slice(startIdx, endIdx + 1);
let currentMediaIds = Object.keys(this.thumbs);
let firstExistingIdx = mediaIds.indexOf(currentMediaIds[0]);
let lastExistingIdx = mediaIds.indexOf(
currentMediaIds[currentMediaIds.length - 1]
);
let incrementalUpdate = false;
if (firstExistingIdx != -1 && lastExistingIdx != -1) {
let currentMediaIdsSubset = mediaIds.slice(
firstExistingIdx,
lastExistingIdx + 1
);
incrementalUpdate = helpers.other.arrayEqual(
currentMediaIdsSubset,
currentMediaIds
);
}
if (!incrementalUpdate) {
this._clearThumbs();
let restoreIdx = mediaIds.indexOf(targetMediaId);
if (restoreIdx != -1) {
lastExistingIdx = restoreIdx - 1;
firstExistingIdx = restoreIdx;
} else {
lastExistingIdx = -1;
firstExistingIdx = 0;
}
}
for (let idx = lastExistingIdx + 1; idx < mediaIds.length; ++idx) {
let mediaId = mediaIds[idx];
let searchPage = mediaIdPages[mediaId];
let node = this.createThumb(mediaId, searchPage);
helpers.other.addToEnd(this.thumbs, mediaId, node);
this.grid.addThumbToRow(node, { atEnd: true });
}
for (let idx = firstExistingIdx - 1; idx >= 0; --idx) {
let mediaId = mediaIds[idx];
let searchPage = mediaIdPages[mediaId];
let node = this.createThumb(mediaId, searchPage);
this.thumbs = helpers.other.addToBeginning(this.thumbs, mediaId, node);
this.grid.addThumbToRow(node, { atEnd: false });
}
this.restoreScrollPosition(savedScroll);
}
_clearThumbs() {
for (let node of Object.values(this.thumbs)) {
node.remove();
for (let observer of this.intersectionObservers)
observer.unobserve(node);
}
this.thumbs = {};
this.grid.clear();
}
createThumb(mediaId, searchPage) {
let entry = this.createTemplate({
name: "template-thumbnail",
makeSVGUnique: false,
html: \`
<div class=thumbnail-box>
<a class=thumbnail-link href=#>
<img class=thumb>
</a>
<div class=last-viewed-image-marker>
<ppixiv-inline class=last-viewed-image-marker src="resources/last-viewed-image-marker.svg"></ppixiv-inline>
</div>
<div class=bottom-row>
<div class=bottom-left-icon>
<div class="heart button-bookmark public bookmarked" hidden>
<ppixiv-inline src="resources/heart-icon.svg"></ppixiv-inline>
</div>
<div class="heart button-bookmark-private bookmarked" hidden>
<ppixiv-inline src="resources/heart-icon-private.svg"></ppixiv-inline>
</div>
<img class=ai-image src="ppixiv:resources/ai.png" hidden width='32' height='32'>
</div>
<div style="flex: 1;"></div>
<div class=thumbnail-label hidden>
<span class="thumbnail-ellipsis-box">
<span class=label></span>
</span>
</div>
<div style="flex: 1;"></div>
<div class=bottom-right-icon>
<div class=ugoira-icon hidden>
<ppixiv-inline src="resources/play-button.svg"></ppixiv-inline>
</div>
<div class=manga-info-box hidden>
<img class="page-icon regular" src="ppixiv:resources/page-icon.png">
<img class="page-icon hover" src="ppixiv:resources/page-icon-hover.png">
<span class=page-count hidden>1234</span>
</div>
</div>
</div>
<div class=muted-text>
\${helpers.createIcon("mat:block", { classes: ["muted-icon"] })}
<span class=muted-label></span>
</div>
</div>
\`
});
entry.dataset.id = mediaId;
if (searchPage != null)
entry.dataset.searchPage = searchPage;
for (let observer of this.intersectionObservers)
observer.observe(entry);
this.setupThumb(entry);
return entry;
}
_thumbnailSize(mediaId) {
let { thumbWidth, thumbHeight, desiredPixels } = this.sizingStyle;
let { type } = helpers.mediaId.parse(mediaId);
if (type != "illust" && type != "file" && type != "folder")
return { thumbWidth, thumbHeight };
if (this.sizingStyle.thumbnailStyle == "square")
return { thumbWidth, thumbHeight };
let aspectRatio = null;
if (this.dataSource?.name == "manga") {
aspectRatio = ppixiv.extraCache.getMediaAspectRatioSync(mediaId);
if (aspectRatio == null) {
console.warn(\`Manga view didn't cache the aspect ratio for \${mediaId}\`);
aspectRatio = 1;
}
} else {
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId, {
full: false
});
if (mediaInfo == null)
throw new Error(\`Missing media info data for \${mediaId}\`);
let { width, height } = ppixiv.mediaCache.getImageDimensions(
mediaInfo,
mediaId
);
if (width == null)
return { thumbWidth, thumbHeight };
aspectRatio = width / height;
}
aspectRatio = helpers.math.clamp(aspectRatio, 1 / 3, 3);
thumbWidth = Math.sqrt(desiredPixels * aspectRatio);
thumbHeight = thumbWidth / aspectRatio;
thumbWidth = Math.round(thumbWidth);
thumbHeight = Math.round(thumbHeight);
return { thumbWidth, thumbHeight };
}
_setThumbnailSize(mediaId, element) {
let { thumbWidth, thumbHeight } = this._thumbnailSize(mediaId);
element.origWidth = thumbWidth;
element.origHeight = thumbHeight;
}
setupThumb(element) {
let mediaId = element.dataset.id;
if (mediaId == null)
return;
let { id: thumbId, type: thumbType } = helpers.mediaId.parse(mediaId);
this.addAnimationListener(element);
this._setThumbnailSize(mediaId, element);
if (thumbType == "user" || thumbType == "bookmarks") {
let userId = thumbId;
let link2 = element.querySelector("a.thumbnail-link");
if (thumbType == "user")
link2.href = \`/users/\${userId}/artworks#ppixiv\`;
else
link2.href = \`/users/\${userId}/bookmarks/artworks#ppixiv\`;
link2.dataset.userId = userId;
let quickUserData = ppixiv.extraCache.getQuickUserData(userId);
if (quickUserData == null) {
throw new Error(\`Missing quick user data for user ID \${userId}\`);
}
let thumb2 = element.querySelector(".thumb");
thumb2.src = quickUserData.profileImageUrl;
let label2 = element.querySelector(".thumbnail-label");
label2.hidden = false;
label2.querySelector(".label").innerText = quickUserData.userName;
return;
}
if (thumbType != "illust" && thumbType != "file" && thumbType != "folder")
throw "Unexpected thumb type: " + thumbType;
let info = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (info == null)
throw new Error(\`Missing media info data for \${mediaId}\`);
let { page } = helpers.mediaId.parse(mediaId);
let url = info.previewUrls[page];
let thumb = element.querySelector(".thumb");
let [illustId, illustPage] = helpers.mediaId.toIllustIdAndPage(mediaId);
let mutedTag = ppixiv.muting.anyTagMuted(info.tagList);
let mutedUser = ppixiv.muting.isUserIdMuted(info.userId);
if (mutedTag || mutedUser) {
thumb.src = ppixiv.mediaCache.getProfilePictureUrl(info.userId);
element.classList.add("muted");
let mutedLabel = element.querySelector(".muted-label");
(async () => {
if (mutedTag)
mutedTag = await ppixiv.tagTranslations.getTranslation(mutedTag);
mutedLabel.textContent = mutedTag ? mutedTag : info.userName;
})();
thumb.dataset.mutedUrl = url;
} else {
thumb.src = url;
element.classList.remove("muted");
LocalAPI.thumbnailWasLoaded(url);
ppixiv.extraCache.registerLoadingThumbnail(mediaId, thumb);
this.thumbImageLoadFinished(element, { cause: "setup" });
}
let link = element.querySelector("a.thumbnail-link");
if (thumbType == "folder") {
let args = new helpers.args("/");
LocalAPI.getArgsForId(mediaId, args);
link.href = args.url;
} else {
link.href = getUrlForMediaId(mediaId).url;
}
link.dataset.mediaId = mediaId;
link.dataset.userId = info.userId;
element.querySelector(".ugoira-icon").hidden = info.illustType != 2 && info.illustType != "video";
helpers.html.setClass(
element,
"dot",
helpers.pixiv.tagsContainDot(info.tagList)
);
this.refreshExpandedThumb(element);
helpers.html.setClass(link, "first-page", illustPage == 0);
helpers.html.setClass(link, "last-page", illustPage == info.pageCount - 1);
link.style.borderBottomColor = \`hsl(\${illustId}deg 50% 50%)\`;
this.refreshBookmarkIcon(element);
let label = element.querySelector(".thumbnail-label");
if (thumbType == "folder") {
let parts = mediaId.split("/");
let basename = parts[parts.length - 1];
let label2 = element.querySelector(".thumbnail-label");
label2.hidden = false;
label2.querySelector(".label").innerText = basename;
} else {
label.hidden = true;
}
}
makeThumbnailSizingStyle() {
let thumbnailStyle = ppixiv.settings.get("thumbnail_style");
let desiredSize = ppixiv.settings.get("thumbnail-size", 4);
desiredSize = MenuOptionsThumbnailSizeSlider.thumbnailSizeForValue(desiredSize);
let padding = ppixiv.mobile ? 3 : 15;
let containerWidth = Math.floor(this.root.getBoundingClientRect().width);
let containerHeight = Math.floor(
this.scrollContainer.getBoundingClientRect().height
);
let columns = containerWidth / desiredSize;
columns = Math.floor(columns);
columns = Math.max(columns, 1);
let remainingWidth = containerWidth - padding * (columns - 1);
let thumbWidth = Math.floor(remainingWidth / columns);
let thumbHeight = Math.floor(thumbWidth);
containerWidth = Math.floor(thumbWidth * columns + padding * (columns - 1));
let maxThumbs = this.dataSource?.isVView ? 500 : 40;
let rows = window.innerHeight / thumbWidth;
if (columns * rows > maxThumbs) {
columns = maxThumbs / rows;
containerWidth = Math.floor(
thumbWidth * columns + padding * (columns - 1)
);
}
let desiredPixels = thumbWidth * thumbHeight;
return {
thumbnailStyle,
padding,
thumbWidth,
thumbHeight,
containerWidth,
containerHeight,
desiredPixels,
deps: [ppixiv.settings.get("pixiv_cdn")]
};
}
sanityCheckThumbList() {
let actual = [];
for (let thumb of this.thumbnailBox.children)
actual.push(thumb.dataset.id);
let expected = Object.keys(this.thumbs);
if (JSON.stringify(actual) != JSON.stringify(expected)) {
console.log("actual ", actual);
console.log("expected", expected);
}
}
thumbnailClick(e) {
let pageCountBox = e.target.closest(".manga-info-box");
if (pageCountBox) {
e.preventDefault();
e.stopPropagation();
let idNode = pageCountBox.closest("[data-id]");
let mediaId = idNode.dataset.id;
this.setMediaIdExpanded(mediaId, !this.isMediaIdExpanded(mediaId));
}
}
saveScrollPosition() {
let firstVisibleThumbNode = this.getFirstFullyOnscreenThumb();
if (firstVisibleThumbNode == null)
return null;
let row = firstVisibleThumbNode.parentNode;
return {
savedScroll: helpers.html.saveScrollPosition(this.scrollContainer, row),
mediaId: firstVisibleThumbNode.dataset.id
};
}
restoreScrollPosition(scroll) {
if (scroll == null)
return false;
let restoreScrollPositionNode = this.getThumbnailForMediaId(scroll.mediaId);
if (restoreScrollPositionNode == null)
return false;
let row = restoreScrollPositionNode.parentNode;
helpers.html.restoreScrollPosition(
this.scrollContainer,
row,
scroll.savedScroll
);
return true;
}
setMediaIdExpanded(mediaId, newValue) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
this.expandedMediaIds.set(mediaId, newValue);
if (this._mediaIdExpandedCache)
this._mediaIdExpandedCache.delete(mediaId);
this.saveExpandedMediaIds();
this.refreshImages({ cause: "manga-expansion-change", purge: true });
if (!newValue) {
this.scrollToMediaId(helpers.mediaId.getMediaIdFirstPage(mediaId));
}
}
toggleExpandingMediaIdsByDefault() {
let args = helpers.args.location;
let newValue = !this.mediaIdsExpandedByDefault;
if (newValue == ppixiv.settings.get("expand_manga_thumbnails"))
args.hash.delete("expand-thumbs");
else
args.hash.set("expand-thumbs", newValue ? "1" : "0");
delete args.state.expandedMediaIds;
helpers.navigate(args);
}
loadExpandedMediaIds() {
let args = helpers.args.location;
let mediaIds = args.state.expandedMediaIds ?? {};
this.expandedMediaIds = new Map(Object.entries(mediaIds));
let expandThumbs = args.hash.get("expand-thumbs");
if (expandThumbs == null)
this.mediaIdsExpandedByDefault = ppixiv.settings.get(
"expand_manga_thumbnails"
);
else
this.mediaIdsExpandedByDefault = expandThumbs == "1";
}
saveExpandedMediaIds() {
let args = helpers.args.location;
args.state.expandedMediaIds = Object.fromEntries(this.expandedMediaIds);
helpers.navigate(args, {
addToHistory: false,
cause: "viewing-page",
sendPopstate: false
});
}
isMediaIdExpanded(mediaId) {
this._mediaIdExpandedCache ??= /* @__PURE__ */ new Map();
if (!this._mediaIdExpandedCache.has(mediaId))
this._mediaIdExpandedCache.set(mediaId, this._isMediaIdExpanded(mediaId));
return this._mediaIdExpandedCache.get(mediaId);
}
_isMediaIdExpanded(mediaId) {
if (!this.dataSource?.allowExpandingMangaPages)
return false;
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
let { type } = helpers.mediaId.parse(mediaId);
if (type != "illust")
return false;
if (this.expandedMediaIds.has(mediaId))
return this.expandedMediaIds.get(mediaId);
if (!this.mediaIdsExpandedByDefault)
return false;
let info = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (info != null) {
let mutedTag = ppixiv.muting.anyTagMuted(info.tagList);
let mutedUser = ppixiv.muting.isUserIdMuted(info.userId);
if (mutedTag || mutedUser)
return false;
}
if (info == null || info.pageCount == 1)
return false;
return true;
}
refreshExpandedThumb(thumb) {
if (thumb == null)
return;
let mediaId = thumb.dataset.id;
let showExpanded = this.dataSource?.allowExpandingMangaPages && this.isMediaIdExpanded(mediaId);
helpers.html.setClass(thumb, "expanded-thumb", showExpanded);
let info = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
let [illustId, illustPage] = helpers.mediaId.toIllustIdAndPage(mediaId);
helpers.html.setClass(thumb, "expanded-manga-post", showExpanded);
helpers.html.setClass(
thumb,
"first-manga-page",
info && info.pageCount > 1 && illustPage == 0
);
let showMangaPage = info && info.pageCount > 1 && this.dataSource?.name != "manga";
let pageCountBox = thumb.querySelector(".manga-info-box");
pageCountBox.hidden = !showMangaPage;
if (showMangaPage) {
let text = showExpanded ? \`\${illustPage + 1}/\${info.pageCount}\` : info.pageCount;
pageCountBox.querySelector(".page-count").textContent = text;
pageCountBox.querySelector(".page-count").hidden = false;
helpers.html.setClass(pageCountBox, "show-expanded", showExpanded);
}
}
refreshExpandedThumbAll() {
for (let thumb of this.getLoadedThumbs())
this.refreshExpandedThumb(thumb);
}
thumbImageLoadFinished(element, { cause }) {
if (element.dataset.thumbLoaded)
return;
let mediaId = element.dataset.id;
let [illustId, illustPage] = helpers.mediaId.toIllustIdAndPage(mediaId);
let thumb = element.querySelector(".thumb");
let width, height;
if (illustPage == 0) {
let info = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (info != null) {
width = info.width;
height = info.height;
}
}
if (width == null && thumb.naturalWidth != 0) {
width = thumb.naturalWidth;
height = thumb.naturalHeight;
}
if (width == null)
return;
if (element.offsetWidth == 0)
return;
element.dataset.thumbLoaded = "1";
let aspectRatio = element.offsetWidth / element.offsetHeight;
SearchView.createThumbnailAnimation(thumb, width, height, aspectRatio);
}
static getThumbnailPanningDirection(thumb, width, height, containerAspectRatio) {
if (width == null || height == null) {
helpers.html.setClass(thumb, "vertical-panning", false);
helpers.html.setClass(thumb, "horizontal-panning", false);
return null;
}
let aspectRatio = width / height;
aspectRatio /= containerAspectRatio;
let minAspectForPan = 1.1;
let maxAspectForPan = 4;
if (aspectRatio > 1 / maxAspectForPan && aspectRatio < 1 / minAspectForPan)
return "vertical";
else if (aspectRatio > minAspectForPan && aspectRatio < maxAspectForPan)
return "horizontal";
else
return null;
}
static createThumbnailAnimation(thumb, width, height, containerAspectRatio) {
if (ppixiv.mobile)
return null;
let direction = this.getThumbnailPanningDirection(
thumb,
width,
height,
containerAspectRatio
);
if (thumb.panAnimation != null || direction == null)
return null;
let keyframes = direction == "horizontal" ? [
{ offset: 0, easing: "ease-in-out", objectPosition: "left top" },
{ offset: 0.4, easing: "ease-in-out", objectPosition: "right top" },
{ offset: 0.5, easing: "ease-in-out", objectPosition: "right top" },
{ offset: 0.9, easing: "ease-in-out", objectPosition: "left top" },
{ offset: 1, easing: "ease-in-out", objectPosition: "left top" }
] : [
{
offset: 0,
easing: "ease-in-out",
objectPosition: "center top"
},
{
offset: 0.4,
easing: "ease-in-out",
objectPosition: "center bottom"
},
{
offset: 0.5,
easing: "ease-in-out",
objectPosition: "center bottom"
},
{
offset: 0.9,
easing: "ease-in-out",
objectPosition: "center top"
},
{
offset: 1,
easing: "ease-in-out",
objectPosition: "center top"
}
];
let animation = new Animation(
new KeyframeEffect(thumb, keyframes, {
duration: 4e3,
iterations: Infinity,
delay: direction == "horizontal" ? -800 : 0
})
);
animation.id = direction == "horizontal" ? "horizontal-pan" : "vertical-pan";
thumb.panAnimation = animation;
return animation;
}
addAnimationListener(element) {
if (ppixiv.mobile)
return;
if (element.addedAnimationListener)
return;
element.addedAnimationListener = true;
element.addEventListener("mouseover", (e) => {
if (ppixiv.settings.get("disable_thumbnail_panning") || ppixiv.settings.get("thumbnail_style") == "aspect" || ppixiv.mobile)
return;
let thumb = element.querySelector(".thumb");
let anim = thumb.panAnimation;
if (anim == null)
return;
anim.play();
if (this.stopAnimation?.animation == anim)
return;
if (this.stopAnimation) {
this.stopAnimation.shutdown();
this.stopAnimation = null;
}
this.stopAnimation = new StopAnimationAfter(
anim,
6,
1,
anim.id == "vertical-pan"
);
element.addEventListener(
"mouseout",
(e2) => {
this.stopAnimation.shutdown();
this.stopAnimation = null;
anim.pause();
},
{ once: true, signal: this.stopAnimation.abort.signal }
);
});
}
refreshThumbnail(mediaId) {
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId, {
full: false
});
if (mediaInfo == null)
return;
let thumbnailElement = this.getThumbnailForMediaId(mediaId);
if (thumbnailElement != null)
this.refreshBookmarkIcon(thumbnailElement);
for (let page = 0; page < mediaInfo.pageCount; ++page) {
let pageMediaId = helpers.mediaId.getMediaIdForPage(mediaId, page);
thumbnailElement = this.getThumbnailForMediaId(pageMediaId);
if (thumbnailElement != null)
this.refreshBookmarkIcon(thumbnailElement);
}
}
refreshHeader() {
let img = this.artistHeader.querySelector("img");
let headerStripURL = this.dataSource?.uiInfo?.headerStripURL;
if (headerStripURL == null) {
this.artistHeader.hidden = true;
img.src = helpers.other.blankImage;
return;
}
if (img.src == headerStripURL)
return;
let savedScroll = this.saveScrollPosition();
helpers.html.setClass(
img,
"animated",
ppixiv.mobile || !ppixiv.settings.get("disable_thumbnail_panning")
);
img.classList.remove("loaded");
img.onload = () => img.classList.add("loaded");
img.src = headerStripURL ?? helpers.other.blankImage;
this.artistHeader.hidden = false;
this.restoreScrollPosition(savedScroll);
}
refreshAllBookmarkIcons() {
for (let thumb of this.getLoadedThumbs())
this.refreshBookmarkIcon(thumb);
}
refreshBookmarkIcon(thumbnailElement) {
if (this.dataSource && this.dataSource.name == "manga")
return;
let mediaId = thumbnailElement.dataset.id;
if (mediaId == null)
return;
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId, {
full: false
});
if (mediaInfo == null)
return;
let showAI = mediaInfo.aiType == 2;
let showBookmarkHeart = mediaInfo.bookmarkData != null;
if (this.dataSource != null && !this.dataSource.showBookmarkIcons)
showBookmarkHeart = false;
if (ppixiv.mobile && showAI && showBookmarkHeart)
showAI = false;
if (!ppixiv.settings.get("show_ai_icon"))
showAI = false;
thumbnailElement.querySelector(".ai-image").hidden = !showAI;
thumbnailElement.querySelector(".button-bookmark").hidden = !showBookmarkHeart || mediaInfo.bookmarkData.private;
thumbnailElement.querySelector(".button-bookmark-private").hidden = !showBookmarkHeart || !mediaInfo.bookmarkData.private;
}
refreshAfterMuteChange() {
this._mediaIdExpandedCache = null;
this.refreshImages({ cause: "mutes-changed", purge: true });
}
getLoadedThumbs() {
return Object.values(this.thumbs);
}
scrollToMediaId(mediaId) {
if (mediaId == null)
return false;
this.refreshImages({ targetMediaId: mediaId, cause: "scroll-to-id" });
let thumb = this.getThumbnailForMediaId(mediaId, { fallbackOnPage1: true });
if (thumb == null)
return false;
this.pulseThumbnail(mediaId);
let { offsetTop, offsetHeight } = thumb.parentNode;
if (offsetTop >= this.scrollContainer.scrollTop && offsetTop + offsetHeight < this.scrollContainer.scrollTop + this.scrollContainer.offsetHeight)
return true;
let y = offsetTop + offsetHeight / 2 - this.scrollContainer.offsetHeight / 2;
y = helpers.math.clamp(
y,
0,
this.scrollContainer.scrollHeight - this.scrollContainer.offsetHeight
);
this.scrollContainer.scrollTop = y;
return true;
}
getRectForMediaId(mediaId) {
let thumb = this.getThumbnailForMediaId(mediaId, { fallbackOnPage1: true });
if (thumb == null)
return null;
return thumb.getBoundingClientRect();
}
pulseThumbnail(mediaId) {
if (ppixiv.settings.get("animations_enabled"))
return;
let thumb = this.getThumbnailForMediaId(mediaId);
if (thumb == null)
return;
this.stopPulsingThumbnail();
this.flashingImage = thumb;
thumb.classList.add("flash");
}
stopPulsingThumbnail() {
if (this.flashingImage == null)
return;
this.flashingImage.classList.remove("flash");
this.flashingImage = null;
}
};
/\x2f web/vview/actors/hover-with-delay.js
var HoverWithDelay = class extends Actor {
constructor({
parent,
element,
enterDelay = 0,
exitDelay = 0
} = {}) {
super({ parent });
this.element = element;
this.enterDelay = enterDelay * 1e3;
this.exitDelay = exitDelay * 1e3;
this.timer = -1;
this.pendingHover = null;
element.addEventListener("mouseenter", (e) => this.onHoverChanged(true), this._signal);
element.addEventListener("mouseleave", (e) => this.onHoverChanged(false), this._signal);
}
onHoverChanged(hovering) {
if (this.pendingHover != null && this.pendingHover == hovering)
return;
if (this.hoverTimeout != null) {
realClearTimeout(this.hoverTimeout);
this.hoverTimeout = null;
}
this.realHoverState = hovering;
this.pendingHover = hovering;
let delay = hovering ? this.enterDelay : this.exitDelay;
this.hoverTimeout = realSetTimeout(() => {
this.pendingHover = null;
this.hoverTimeout = null;
helpers.html.setClass(this.element, "hover", this.realHoverState);
}, delay);
}
};
/\x2f web/vview/screen-search/screen-search.js
var ScreenSearch = class extends Screen {
get screenType() {
return "search";
}
constructor(options) {
super({ ...options, template: \`
<div inert class="screen screen-search-container">
<!-- The tree widget for local navigation: -->
<div class=local-navigation-box hidden></div>
<vv-container class=search-mobile-ui></vv-container>
<div class="search-results scroll-container">
<div class=search-desktop-ui hidden></div>
<vv-container class=thumbnail-container-box></vv-container>
</div>
<div class=mobile-navigation-bar-container></div>
</div>
\` });
ppixiv.userCache.addEventListener("usermodified", this.refreshUi, { signal: this.shutdownSignal });
this.searchView = new SearchView({
container: this.root.querySelector(".thumbnail-container-box")
});
if (!ppixiv.mobile) {
let searchDesktopUiBox = this.root.querySelector(".search-desktop-ui");
searchDesktopUiBox.hidden = false;
this.desktopSearchUi = new DesktopSearchUI({
container: searchDesktopUiBox
});
new HoverWithDelay({ parent: this, element: searchDesktopUiBox, enterDelay: 0, exitDelay: 0.25 });
let resize = new ResizeObserver(() => {
searchDesktopUiBox.style.setProperty("--ui-box-height", \`\${searchDesktopUiBox.offsetHeight}px\`);
}).observe(searchDesktopUiBox);
this.shutdownSignal.addEventListener("abort", () => resize.disconnect());
let refreshUiOnHover = () => helpers.html.setClass(
searchDesktopUiBox,
"ui-on-hover",
ppixiv.settings.get("ui-on-hover") && !ppixiv.mobile
);
ppixiv.settings.addEventListener("ui-on-hover", refreshUiOnHover, { signal: this.shutdownSignal });
refreshUiOnHover();
}
if (ppixiv.mobile) {
this.mobileSearchUi = new SearchUIMobile({
container: this.root.querySelector(".search-mobile-ui")
});
let navigationBarContainer = this.root.querySelector(".mobile-navigation-bar-container");
this.mobileMenuBar = new MobileSearchUI({
container: navigationBarContainer
});
helpers.html.setSizeAsProperty(this.mobileSearchUi.root, {
...this._signal,
heightProperty: "--title-height",
target: this.root
});
helpers.html.setSizeAsProperty(this.mobileMenuBar.root, {
...this._signal,
heightProperty: "--nav-bar-height",
target: this.root
});
let scroller = this.querySelector(".search-results");
this.scrollListener = new ScrollListener({
scroller,
parent: this,
onchange: () => this._refreshMenuBarVisible(),
stickyUiNode: this.mobileSearchUi.root
});
OpenWidgets.singleton.addEventListener("changed", () => this._refreshMenuBarVisible(), this._signal);
this._refreshMenuBarVisible();
}
this.root.addEventListener("wheel", (e) => {
if (!e.ctrlKey)
return;
e.preventDefault();
e.stopImmediatePropagation();
ppixiv.settings.adjustZoom("thumbnail-size", e.deltaY > 0);
}, { passive: false });
this.root.addEventListener("keydown", (e) => {
let zoom = helpers.isZoomHotkey(e);
if (zoom != null) {
e.preventDefault();
e.stopImmediatePropagation();
ppixiv.settings.adjustZoom("thumbnail-size", zoom < 0);
}
});
if (LocalAPI.isEnabled() && !LocalAPI.localInfo.bookmark_tag_searches_only && !ppixiv.mobile) {
let localNavigationBox = this.root.querySelector(".local-navigation-box");
this._localNavigationVisible = !ppixiv.mobile && ppixiv.plocation.pathname != "/similar";
this._localNavigationTree = new LocalNavigationTreeWidget({
container: localNavigationBox
});
localNavigationBox.hidden = false;
}
}
get active() {
return this._active;
}
deactivate() {
super.deactivate();
if (!this._active)
return;
this._active = false;
this.searchView.deactivate();
}
async activate() {
super.activate();
this._active = true;
this.refreshUi();
await this.searchView.activate();
}
getTargetMediaId(args) {
let scroll = args.state.scroll;
let targetMediaId = scroll?.scrollPosition?.mediaId;
return targetMediaId;
}
getRectForMediaId(mediaId) {
return this.searchView.getRectForMediaId(mediaId);
}
setDataSource(dataSource, { targetMediaId }) {
if (this.dataSource != null)
this.dataSource.removeEventListener("updated", this.dataSourceUpdated);
this.dataSource = dataSource;
this.searchView.setDataSource(dataSource, { targetMediaId });
if (this.desktopSearchUi)
this.desktopSearchUi.setDataSource(dataSource);
if (this.mobileSearchUi)
this.mobileSearchUi.setDataSource(dataSource);
if (this.dataSource == null) {
this.refreshUi();
return;
}
this.dataSource.addEventListener("updated", this.dataSourceUpdated);
this.refreshUi();
}
dataSourceUpdated = () => {
this.refreshUi();
};
refreshSearch() {
ppixiv.app.setCurrentDataSource({ refresh: true, startAtBeginning: true });
}
refreshSearchFromPage() {
ppixiv.app.setCurrentDataSource({ refresh: true, startAtBeginning: false });
}
refreshUi = () => {
if (this.desktopSearchUi)
this.desktopSearchUi.refreshUi();
if (this.mobileSearchUi)
this.mobileSearchUi.refreshUi();
if (this.mobileMenuBar)
this.mobileMenuBar.refreshUi();
this.dataSource.setPageIcon();
if (this.active)
helpers.setPageTitle(this.dataSource.pageTitle || "Loading...");
helpers.html.setDataSet(this.root.dataset, "showNavigation", this.canShowLocalNavigation && this._localNavigationVisible);
};
_refreshMenuBarVisible() {
let shown = !this.scrollListener.scrolledForwards;
this.mobileMenuBar.visible = shown && OpenWidgets.singleton.empty;
this.mobileSearchUi.visible = shown;
}
get canShowLocalNavigation() {
return this.dataSource?.isVView && !LocalAPI?.localInfo?.bookmark_tag_searches_only;
}
get viewingUserId() {
if (this.dataSource == null)
return null;
return this.dataSource.viewingUserId;
}
get displayedMediaId() {
if (this.dataSource == null)
return super.displayedMediaId;
let mediaId = this.dataSource.uiInfo.mediaId;
if (mediaId != null)
return mediaId;
return super.displayedMediaId;
}
async handleKeydown(e) {
if (e.repeat)
return;
if (this.dataSource.name == "vview" || this.dataSource.name == "vview-search") {
if (e.code == "KeyF" && e.ctrlKey) {
this.root.querySelector(".local-tag-search-box input").focus();
e.preventDefault();
e.stopPropagation();
}
if (e.code == "KeyV" && e.ctrlKey) {
let text = await navigator.clipboard.readText();
let input = this.root.querySelector(".local-tag-search-box input");
input.value = text;
LocalAPI.navigateToTagSearch(text, { addToHistory: false });
}
}
}
};
/\x2f web/vview/misc/fix-chrome-clicks.js
var FixChromeClicks = class {
constructor(container) {
this.enabled = navigator.userAgent.indexOf("Chrome") != -1 && !ppixiv.mobile;
if (!this.enabled)
return;
this.root = container;
this.pressedNode = null;
this.root.addEventListener("pointerdown", this.onpointer, true);
this.root.addEventListener("pointerup", this.onpointer, true);
this.root.addEventListener("pointermove", this.onpointer, true);
this.root.addEventListener("contextmenu", this.oncontextmenu);
this.root.addEventListener("click", this.onclick, true);
}
startWaitingForRelease() {
if (this.pressedNode != null) {
console.warn("Unexpected call to startWaitingForRelease");
return;
}
window.addEventListener("pointerup", this.onpointer, true);
window.addEventListener("pointermove", this.onpointer, true);
}
stopWaitingForRelease() {
if (this.pressedNode == null)
return;
window.removeEventListener("pointerup", this.onpointer, true);
window.removeEventListener("pointermove", this.onpointer, true);
this.pressedNode = null;
}
onpointer = (e) => {
if (e.type == "pointerdown") {
this.startWaitingForRelease();
}
if (e.buttons & 1) {
if (this.pressedNode == null) {
this.pressedNode = e.target;
}
return;
}
if (this.pressedNode == null)
return;
let pressedNode = this.pressedNode;
this.stopWaitingForRelease();
let releasedNode = e.target;
let clickTarget = null;
if (helpers.html.isAbove(releasedNode, pressedNode))
clickTarget = releasedNode;
else if (helpers.html.isAbove(pressedNode, releasedNode))
clickTarget = pressedNode;
if (clickTarget == null) {
return;
}
if (helpers.html.isAbove(clickTarget, this.root))
return;
e.preventDefault();
this.sendClickEvent(clickTarget, e);
};
oncontextmenu = (e) => {
if (this.pressedNode != null && !e.defaultPrevented) {
console.log("Not sending click because the context menu was opened");
this.pressedNode = null;
}
};
onclick = (e) => {
if (e.button != 0)
return;
if (!e.isTrusted)
return;
e.preventDefault();
e.stopImmediatePropagation();
};
sendClickEvent(target, sourceEvent) {
let e = new MouseEvent("click", sourceEvent);
e.synthetic = true;
target.dispatchEvent(e);
}
shutdown() {
if (!this.enabled)
return;
this.stopWaitingForRelease();
this.pressedNode = null;
this.root.removeEventListener("pointerup", this.onpointer, true);
this.root.removeEventListener("pointerdown", this.onpointer, true);
this.root.removeEventListener("pointermove", this.onpointer, true);
this.root.removeEventListener("contextmenu", this.oncontextmenu);
this.root.removeEventListener("click", this.onclick, true);
}
};
/\x2f web/vview/widgets/local-widgets.js
var LocalSearchBoxWidget = class extends Widget {
constructor({ ...options }) {
super({
...options,
template: \`
<div class="search-box local-tag-search-box">
<div class="input-field-container hover-menu-box">
<input placeholder="Search files" size=1 autocorrect=off>
<span class="clear-local-search-button right-side-button">
\${helpers.createIcon("clear")}
</span>
<span class="submit-local-search-button right-side-button">
\${helpers.createIcon("search")}
</span>
</div>
</div>
\`
});
this.inputElement = this.root.querySelector(".input-field-container > input");
this.dropdownOpener = new DropdownBoxOpener({
button: this.inputElement,
createDropdown: ({ ...options2 }) => {
return new LocalSearchDropdownWidget({
inputElement: this.root,
focusParent: this.root,
...options2
});
},
shouldCloseForClick: (e) => {
if (helpers.html.isAbove(this.root, e.target))
return false;
return true;
}
});
this.inputElement.addEventListener("keydown", (e) => {
if (e.key == "Escape") {
this.dropdownOpener.visible = false;
this.inputElement.blur();
}
});
this.inputElement.addEventListener("focus", () => this.dropdownOpener.visible = true);
this.inputElement.addEventListener("submit", this.submitSearch);
this.clearSearchButton = this.root.querySelector(".clear-local-search-button");
this.clearSearchButton.addEventListener("click", (e) => {
this.inputElement.value = "";
this.inputElement.dispatchEvent(new Event("submit"));
});
this.root.querySelector(".submit-local-search-button").addEventListener("click", (e) => {
this.inputElement.dispatchEvent(new Event("submit"));
});
this.inputElement.addEventListener("input", (e) => {
this.refreshClearButtonVisibility();
});
helpers.inputHandler(this.inputElement, this.submitSearch);
window.addEventListener("pp:popstate", (e) => {
this.refreshFromLocation();
});
this.refreshFromLocation();
this.refreshClearButtonVisibility();
}
visibilityChanged() {
super.visibilityChanged();
if (!this.visibleRecursively)
this.dropdownOpener.visible = false;
}
refreshFromLocation() {
let args = helpers.args.location;
this.inputElement.value = args.hash.get("search") || "";
this.refreshClearButtonVisibility();
}
refreshClearButtonVisibility() {
this.clearSearchButton.hidden = this.inputElement.value == "";
}
submitSearch = (e) => {
let tags = this.inputElement.value;
LocalAPI.navigateToTagSearch(tags);
if (e.target instanceof HTMLInputElement) {
e.target.blur();
this.dropdownOpener.visible = false;
}
};
};
var LocalSearchDropdownWidget = class extends Widget {
constructor({ inputElement, focusParent, ...options }) {
super({ ...options, template: \`
<div class="search-history input-dropdown">
<div class="input-dropdown-contents input-dropdown-list">
<!-- template-tag-dropdown-entry instances will be added here. -->
</div>
</div>
\` });
this.inputElement = inputElement;
this.focusParent = focusParent;
window.addEventListener("recent-local-searches-changed", this._populateDropdown);
this.root.addEventListener("click", this.dropdownClick);
this._inputDropdown = this.root.querySelector(".input-dropdown-list");
let refreshDropdownWidth = () => {
let width = ppixiv.settings.get("tag-dropdown-width", "400");
width = parseInt(width);
if (isNaN(width))
width = 400;
this.root.style.setProperty("--width", \`\${width}px\`);
};
let observer = new MutationObserver((mutations) => {
ppixiv.settings.set("tag-dropdown-width", this._inputDropdown.style.width);
});
observer.observe(this._inputDropdown, { attributes: true });
refreshDropdownWidth();
this._load();
}
dropdownClick = (e) => {
let removeEntry = e.target.closest(".remove-history-entry");
if (removeEntry != null) {
e.stopPropagation();
e.preventDefault();
let tag = e.target.closest(".entry").dataset.tag;
this._removeRecentLocalSearch(tag);
return;
}
if (e.target.closest(".tag"))
this.hide();
};
_removeRecentLocalSearch(search) {
let recentTags = ppixiv.settings.get("local_searches") || [];
while (1) {
let idx = recentTags.indexOf(search);
if (idx == -1)
break;
recentTags.splice(idx, 1);
}
ppixiv.settings.set("local_searches", recentTags);
window.dispatchEvent(new Event("recent-local-searches-changed"));
}
_load() {
this._populateDropdown();
}
createEntry(search) {
let entry = this.createTemplate({ name: "tag-dropdown-entry", html: \`
<a class=entry href=#>
<span class=search></span>
<span class="right-side-buttons">
<span class="remove-history-entry right-side-button keep-menu-open">X</span>
</span>
</a>
\` });
entry.dataset.tag = search;
let span = document.createElement("span");
span.innerText = search;
entry.querySelector(".search").appendChild(span);
let args = new helpers.args("/", ppixiv.plocation);
args.path = LocalAPI.path;
args.hashPath = "/";
args.hash.set("search", search);
entry.href = args.url;
return entry;
}
_populateDropdown = () => {
let tagSearches = ppixiv.settings.get("local_searches") || [];
tagSearches.sort();
let list = this.root.querySelector(".input-dropdown-list");
helpers.html.removeElements(list);
for (let tag of tagSearches) {
let entry = this.createEntry(tag);
entry.classList.add("history");
list.appendChild(entry);
}
};
};
var ViewInExplorerWidget = class extends IllustWidget {
constructor({ ...options }) {
super({
...options,
template: \`
<a href=# class="button private popup local-link">
\${helpers.createIcon("description")}
</a>
\`
});
this.enabled = false;
this.root.addEventListener("click", (e) => {
if (!this.enabled) {
e.preventDefault();
e.stopPropagation();
return;
}
if (e.altKey) {
e.preventDefault();
e.stopPropagation();
let { mediaInfo } = this.getMediaInfo.info;
let localPath = mediaInfo?.localPath;
console.log("f", localPath);
navigator.clipboard.writeText(localPath);
ppixiv.message.show("Path copied to clipboard");
return;
}
});
}
refreshInternal({ mediaId, mediaInfo }) {
let path = mediaInfo?.localPath;
this.enabled = mediaInfo?.localPath != null;
helpers.html.setClass(this.root, "enabled", this.enabled);
if (path == null)
return;
path = path.replace(/\\\\/g, "/");
let url = new URL("vview:/\x2fview-in-explorer");
url.search = path;
url = url.toString();
let a = this.root;
a.href = url;
console.log(url.toString());
let { type } = helpers.mediaId.parse(mediaId);
let popup = type == "file" ? "View file in Explorer" : "View folder in Explorer";
a.dataset.popup = popup;
}
};
/\x2f web/vview/context-menu.js
var ContextMenuTemplate = \`
<div id="popup-context-menu">
<div id="context-menu-image-info">
<vv-container class="context-menu-item avatar-widget-container data-popup=" Loading..."></vv-container>
<div id="context-menu-image-info-container context-menu-item"></div>
</div>
<div id="context-menu-buttons-group">
<div class="like-button-container"></div>
<div class="bookmark-button-container" data-bookmark-type=public>
</div>
<div class="bookmark-button-container" data-bookmark-type=private>
</div>
<div class="context-menu-item button button-fullscreen enabled" data-popup="Fullscreen" style="display:none">
<ppixiv-inline class="ctx-icon" src="resources/fullscreen.svg"></ppixiv-inline>
</div>
<div class="context-menu-item button requires-zoom button-zoom" data-popup="Mousewheel to zoom"
style="display:none">
<ppixiv-inline src="resources/zoom-plus.svg"></ppixiv-inline>
<ppixiv-inline src="resources/zoom-minus.svg"></ppixiv-inline>
</div>
<div class="context-menu-item button requires-zoom button-zoom-level" data-level="cover"
data-popup="Zoom to cover" style="display:none">
<ppixiv-inline class="ctx-icon" src="resources/zoom-full.svg" style="margin-right: 4px;"></ppixiv-inline>
</div>
<div class="context-menu-item button requires-zoom button-zoom-level" data-level="actual"
data-popup="Zoom to actual size">
<ppixiv-inline class="ctx-icon" src="resources/zoom-actual.svg"></ppixiv-inline>
</div>
<a href=# class="button button-view-manga context-menu-item" data-popup="View manga pages">
\${helpers.createIcon("ppixiv:thumbnails", { classes: ["manga"] })}
\${helpers.createIcon("mat:menu_book", { classes: ["series"] })}
</a>
<div class="context-menu-item button button-more enabled" data-popup="More...">
\${helpers.createIcon("settings")}
</div>
<div class="context-menu-item button button-browser-back enabled" data-popup="Back">
<ppixiv-inline class="ctx-icon" src="resources/exit-icon.svg" style="transform: scaleX(-1); margin-right: 4px;">
</ppixiv-inline>
</div>
<div class="context-menu-item button button-parent-folder enabled" data-popup="Parent folder" hidden>
\${helpers.createIcon("folder")}
</div>
<div class="context-menu-item button button-bookmark-tags" data-popup="Bookmark tags" hidden>
\${helpers.createIcon("ppixiv:tag")}
</div>
<div class="context-menu-item view-in-explorer" hidden></div>
</div>
</div>
\`;
var ContextMenu = class extends Widget {
buttons = ["lmb", "rmb", "mmb"];
constructor({ ...options }) {
super({
...options,
template: ContextMenuTemplate
});
this.visible = false;
this.hide = this.hide.bind(this);
this._currentViewer = null;
this._mediaId = null;
this._buttonsDown = {};
if (ppixiv.mobile)
return;
this.getUserIdFromMediaId = new GetUserIdFromMediaId({
parent: this,
onrefresh: ({ userId }) => {
this._cachedUserId = userId;
this.refresh();
}
});
this.root.ontransitionend = () => this.callVisibilityChanged();
this._initButtonListener();
this._buttonViewManga = this.root.querySelector(".button-view-manga");
this._buttonFullscreen = this.root.querySelector(".button-fullscreen");
this._buttonFullscreen.addEventListener("click", this._clickedFullscreen);
this.root.querySelector(".button-zoom").addEventListener("click", this._clickedToggleZoom);
this.root.querySelector(".button-browser-back").addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
ppixiv.phistory.back();
});
this.root.addEventListener("click", this._handleLinkClick);
this.root.querySelector(".button-parent-folder").addEventListener("click", this.clicked_go_to_parent);
for (const button of this.root.querySelectorAll(".button-zoom-level"))
button.addEventListener("click", this._clickedZoomLevel);
this.avatarWidget = new AvatarWidget({
container: this.root.querySelector(".avatar-widget-container"),
mode: "overlay"
});
this._createMoreOptionsButtons();
this.getMediaInfo = this._getMediaInfo();
this.illustWidgets = this._createIllustWidget();
this._createCtxMenuItemDescription();
this.refresh();
}
_initButtonListener() {
this.pointerListener = new PointerListener({
element: window,
buttonMask: 3,
callback: this.pointerEvent
});
window.addEventListener("keydown", this._onKeyEvent);
window.addEventListener("keyup", this._onKeyEvent);
new KeyListener("Control", this._ctrlWasPressed);
new FixChromeClicks(this.root);
window.addEventListener("pp:popstate", (e) => {
if (this._clickedMediaId == null)
return;
this._setTemporaryIllust(null);
});
}
_createCtxMenuItemDescription() {
const elements = this.root.querySelectorAll(".context-menu-item");
for (const element of elements) {
if (element._descriptionObserverAttached)
continue;
element._descriptionObserverAttached = true;
let descEl = element.querySelector(".context-menu-item-description");
if (!descEl) {
descEl = document.createElement("span");
descEl.className = "context-menu-item-description";
element.appendChild(descEl);
}
descEl.textContent = element.dataset.popup || "";
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === "attributes" && mutation.attributeName === "data-popup") {
descEl.textContent = element.dataset.popup || "";
}
}
});
observer.observe(element, { attributes: true });
}
}
_updateAuthorName(mediaId) {
const authorName = this.root.querySelector(".avatar-widget-container");
if (authorName) {
const name = this._userInfo?.name ?? (!helpers.mediaId.isLocal(mediaId) ? "---" : "");
authorName.dataset.popup = name;
}
}
_createMoreOptionsButtons() {
const moreOptionsButton = this.root.querySelector(".button-more");
this._moreOptionsDropdownOpener = new DropdownBoxOpener({
button: moreOptionsButton,
createDropdown: ({ ...options }) => {
const dropdown = new MoreOptionsDropdown({
...options,
parent: this,
showExtra: this.altPressed
});
dropdown.root.classList.add("popup-more-options-dropdown");
dropdown.setMediaId(this._effectiveMediaId);
dropdown.setUserId(this._effectiveUserId);
return dropdown;
}
});
moreOptionsButton.addEventListener("click", (e) => {
this.altPressed = e.altKey;
this._moreOptionsDropdownOpener.visible = !this._moreOptionsDropdownOpener.visible;
});
}
_getMediaInfo() {
const mediaInfo = new GetMediaInfo({
parent: this,
neededData: "full",
onrefresh: async ({ mediaInfo: mediaInfo2 }) => {
this._createManageViewButton(mediaInfo2);
}
});
return mediaInfo;
}
_createManageViewButton(mediaInfo) {
const seriesId = mediaInfo?.seriesNavData?.seriesId;
this._buttonViewManga.hidden = !(mediaInfo?.pageCount > 1 || seriesId != null);
this._buttonViewManga.dataset.popup = mediaInfo == null ? "" : seriesId != null ? "View series" : "View manga pages";
const enabled = seriesId != null || mediaInfo?.pageCount > 1;
helpers.html.setClass(this._buttonViewManga, "enabled", enabled);
this._buttonViewManga.style.pointerEvents = enabled ? "" : "none";
this._buttonViewManga.querySelector(".manga").hidden = seriesId != null;
this._buttonViewManga.querySelector(".series").hidden = seriesId == null;
if (enabled) {
if (seriesId != null) {
const args = new helpers.args("/", ppixiv.plocation);
args.path = \`/user/\${mediaInfo.userId}/series/\${seriesId}\`;
this._buttonViewManga.href = args.url.toString();
} else {
const args = getUrlForMediaId(mediaInfo?.mediaId, { manga: true });
this._buttonViewManga.href = args.url.toString();
}
}
}
_createIllustWidget() {
const illustWidgets = [
this.avatarWidget,
new LikeButtonWidget({
container: this.root.querySelector(".like-button-container"),
template: \`
<vv-container class="context-menu-item button-container button-like-container" data-popup="Like Image">
<ppixiv-inline class="ctx-icon" src="resources/like-button.svg"></ppixiv-inline>
</vv-container>
\`
}),
new ImageInfoWidget({
container: document?.getElementById?.(
"context-menu-image-info-container"
)
})
];
if (ppixiv.native) {
const viewInExplorer = this.root.querySelector(".view-in-explorer");
viewInExplorer.hidden = false;
illustWidgets.push(
new ViewInExplorerWidget({
container: viewInExplorer
})
);
}
this.bookmarkButtons = [];
for (const a of this.root.querySelectorAll("[data-bookmark-type]")) {
const type = a.dataset.bookmarkType;
const bookmarkWidget = new BookmarkButtonWidget({
container: a,
template: \`
<vv-container class="context-menu-item button-bookmark" data-popup="\${type === "public" ? "Bookmark Image" : "Bookmark Privately"}">
<ppixiv-inline class="ctx-icon" src="resources/heart-icon\${type === "private" ? "-private" : ""}.svg"></ppixiv-inline>
</vv-container>
\`,
bookmarkType: type
});
this.bookmarkButtons.push(bookmarkWidget);
illustWidgets.push(bookmarkWidget);
}
this.bookmarkTagsDropdownOpener = new BookmarkTagDropdownOpener({
parent: this,
bookmarkTagsButton: this.root.querySelector(".button-bookmark-tags"),
bookmarkButtons: this.bookmarkButtons
});
illustWidgets.push(this.bookmarkTagsDropdownOpener);
return illustWidgets;
}
_isContextMenuOpen(element) {
const target = element.closest("[data-context-menu-target]");
if (target == null || target.dataset.contextMenuTarget === "off")
return false;
return true;
}
pointerEvent = (e) => {
if (e.pressed) {
if (!this.visible && !this._isContextMenuOpen(e.target))
return;
if (!this.visible && e.mouseButton !== 1)
return;
const buttonName = this.buttons[e.mouseButton];
if (buttonName != null)
this._buttonsDown[buttonName] = true;
if (e.mouseButton !== 1)
return;
this.shiftWasPressed = e.shiftKey;
if (navigator.userAgent.indexOf("Firefox/") === -1 && ppixiv.settings.get("invert-popup-hotkey"))
this.shiftWasPressed = !this.shiftWasPressed;
if (this.shiftWasPressed)
return;
e.preventDefault();
e.stopPropagation();
if (this.touchpadMode && this.visible)
this.hide();
else
this.show({ x: e.clientX, y: e.clientY, target: e.target });
} else {
if (!this.visible)
return;
const buttonName = this.buttons[e.mouseButton];
if (buttonName != null)
this._buttonsDown[buttonName] = false;
this._hideIfAllButtonsReleased();
}
};
get touchpadMode() {
return ppixiv.settings.get("touchpad_mode", false);
}
_onKeyEvent = (e) => {
if (e.repeat)
return;
if (e.target.closest("input, textarea, [contenteditable]"))
return;
if (this._handleKeyEvent(e)) {
e.preventDefault();
e.stopPropagation();
return;
}
};
_getHoveredElement() {
const x = PointerListener.latestMouseClientPosition[0];
const y = PointerListener.latestMouseClientPosition[1];
return document.elementFromPoint(x, y);
}
_ctrlWasPressed = (down) => {
if (!ppixiv.settings.get("ctrl_opens_popup"))
return;
this._buttonsDown.Control = down;
if (down) {
const x = PointerListener.latestMouseClientPosition[0];
const y = PointerListener.latestMouseClientPosition[1];
const node = this._getHoveredElement();
this.show({ x, y, target: node });
} else {
this._hideIfAllButtonsReleased();
}
};
_hideIfAllButtonsReleased() {
if (this.touchpadMode)
return;
if (!this._buttonsDown.lmb && !this._buttonsDown.rmb && !this._buttonsDown.Control)
this.hide();
}
_windowBlur = (e) => {
this.hide();
};
show({ x, y, target }) {
const widget = Widget.fromNode(target, { allowNone: true });
this._currentViewer = null;
if (widget) {
for (const parent of widget.ancestors({ includeSelf: true })) {
if (parent.zoomToggle != null) {
this._currentViewer = parent;
break;
}
}
}
if (this._currentViewer != null)
this._currentViewer.stopDragging();
let mediaId;
if (target != null) {
const result = ppixiv.app.getMediaIdAtElement(target);
if (result != null) {
({ mediaId } = result);
this._setTemporaryIllust(mediaId);
}
}
if (this.visible)
return;
this.pointerListener.checkMissedClicks();
this.displayedMenu = this.root;
this.visible = true;
this.applyVisibility();
OpenWidgets.singleton.set(this, true);
ClassFlags.get.set("hide-ui", true);
window.addEventListener("blur", this._windowBlur);
window.addEventListener("dragstart", this.cancelEvent, true);
if (this.touchpadMode && this.clickOutsideListener == null) {
this.clickOutsideListener = new ClickOutsideListener([this.root], () => {
this.hide();
});
}
let centeredElement = this.elementToCenter;
if (centeredElement == null)
centeredElement = this.displayedMenu;
const pos = helpers.html.getRelativePosition(
centeredElement,
this.displayedMenu
);
pos[0] += centeredElement.offsetWidth / 2;
pos[1] += centeredElement.offsetHeight * 3 / 4;
x -= pos[0];
y -= pos[1];
this.popupPosition = { x, y };
this.setCurrentPosition();
this.displayedMenu.style.transformOrigin = \`\${pos[0]}px \${pos[1]}px\`;
HideMouseCursorOnIdle.disableAll("contextMenu");
this.refresh();
}
setCurrentPosition() {
let { x, y } = this.popupPosition;
if (this._currentViewer == null) {
const windowWidth = window.innerWidth - 4;
const windowHeight = window.innerHeight - 20;
x = helpers.math.clamp(
x,
0,
windowWidth - this.displayedMenu.offsetWidth
);
y = helpers.math.clamp(
y,
0,
windowHeight - this.displayedMenu.offsetHeight
);
}
this.displayedMenu.style.left = \`\${x}px\`;
this.displayedMenu.style.top = \`\${y}px\`;
}
get hideTemporarily() {
return this._hiddenTemporarily;
}
set hideTemporarily(value) {
this._hiddenTemporarily = value;
this.callVisibilityChanged();
}
get actuallyVisible() {
if (this.visible)
return true;
if (this.root.getAnimations().length > 0)
return true;
return false;
}
visibilityChanged(value) {
super.visibilityChanged(value);
OpenWidgets.singleton.set(this, this.visible);
if (this.visible)
window.addEventListener("wheel", this.onwheel, {
capture: true,
passive: false
});
else
window.removeEventListener("wheel", this.onwheel, true);
}
applyVisibility() {
const visible = this.visible && !this._hiddenTemporarily;
helpers.html.setClass(this.root, "hidden-widget", !visible);
helpers.html.setClass(this.root, "visible", visible);
}
hide() {
if (window.keepContextMenuOpen)
return;
if (!this.visible)
return;
this.visible = false;
this._hiddenTemporarily = false;
this.applyVisibility();
OpenWidgets.singleton.set(this, false);
this.displayedMenu = null;
HideMouseCursorOnIdle.enableAll("contextMenu");
this._buttonsDown = {};
ClassFlags.get.set("hide-ui", false);
window.removeEventListener("blur", this._windowBlur);
window.removeEventListener("dragstart", this.cancelEvent, true);
if (this.clickOutsideListener) {
this.clickOutsideListener.shutdown();
this.clickOutsideListener = null;
}
}
cancelEvent = (e) => {
e.preventDefault();
e.stopPropagation();
};
_handleLinkClick = (e) => {
if (!ppixiv.settings.get("ctrl_opens_popup"))
return;
const a = e.target.closest("A");
if (a == null)
return;
if (e.defaultPrevented)
return;
if (e.altKey || e.shiftKey || !e.ctrlKey)
return;
e.preventDefault();
e.stopPropagation();
const url = new URL(a.href, ppixiv.plocation);
helpers.navigate(url);
};
get _effectiveMediaId() {
const mediaId = this._clickedMediaId ?? this._mediaId;
if (mediaId == null)
return null;
const { type } = helpers.mediaId.parse(mediaId);
if (type === "user")
return null;
return mediaId;
}
get _effectiveUserId() {
return this.getUserIdFromMediaId.info.userId;
}
setMediaId(mediaId) {
if (this._mediaId === mediaId)
return;
this._mediaId = mediaId;
this.getUserIdFromMediaId.id = this._clickedMediaId ?? this._mediaId;
this.refresh();
}
get elementToCenter() {
return this.displayedMenu.querySelector(".button-zoom");
}
get _isZoomUiEnabled() {
return this._currentViewer != null && this._currentViewer.slideshowMode == null;
}
setDataSource(dataSource) {
if (this.dataSource === dataSource)
return;
this.dataSource = dataSource;
for (const widget of this.illustWidgets) {
if (widget.setDataSource)
widget.setDataSource(dataSource);
}
this.refresh();
}
_handleKeyEventForImage(e) {
let mediaId = this._effectiveMediaId;
const screenName = ppixiv.app.getDisplayedScreen();
if (mediaId == null) {
const node = this._getHoveredElement();
mediaId = ppixiv.app.getMediaIdAtElement(node).mediaId;
}
if (e.code === "Escape" && !e.ctrlKey && !e.altKey && !e.shiftKey && screenName === "illust") {
ppixiv.phistory.back();
return true;
}
if (LocalAPI.isVVbrowser()) {
if (e.altKey && e.key === "ArrowLeft") {
navigation.back();
e.preventDefault();
} else if (e.altKey && e.key === "ArrowRight") {
navigation.forward();
e.preventDefault();
}
}
if (!e.ctrlKey)
return;
if (e.key.toUpperCase() === "V") {
if (mediaId == null)
return;
Actions.likeImage(mediaId);
return true;
}
if (e.key.toUpperCase() === "B") {
(async () => {
if (mediaId == null)
return;
const mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, {
full: false
});
if (e.altKey && e.shiftKey) {
Actions.addNewBookmarkTag(mediaId);
return;
}
if (e.shiftKey) {
if (mediaInfo.bookmarkData == null) {
ppixiv.message.show("Image isn't bookmarked");
return;
}
Actions.bookmarkRemove(mediaId);
return;
}
let bookmarkPrivately = null;
if (e.altKey)
bookmarkPrivately = true;
if (mediaInfo.bookmarkData != null) {
ppixiv.message.show("Already bookmarked (^B to remove bookmark)");
return;
}
Actions.bookmarkAdd(mediaId, {
private: bookmarkPrivately
});
})();
return true;
}
if (e.key.toUpperCase() === "P") {
const enable = !ppixiv.settings.get("auto_pan", false);
ppixiv.settings.set("auto_pan", enable);
ppixiv.message.show(\`Image panning \${enable ? "enabled" : "disabled"}\`);
return true;
}
if (e.key.toUpperCase() === "S") {
(async () => {
if (mediaId == null)
return;
const mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, {
full: false
});
let downloadType = "image";
if (Actions.isDownloadTypeAvailable("image", mediaInfo))
downloadType = "image";
else if (Actions.isDownloadTypeAvailable("MKV", mediaInfo))
downloadType = "MKV";
if (e.altKey && Actions.isDownloadTypeAvailable("ZIP", mediaInfo))
downloadType = "ZIP";
Actions.downloadIllust(mediaId, downloadType);
})();
return true;
}
return false;
}
_handleKeyEventForUser(e) {
const userId = this._effectiveUserId;
if (!e.ctrlKey)
return;
if (e.key.toUpperCase() === "F") {
(async () => {
if (userId == null)
return;
const userInfo = this._userInfo;
if (userInfo == null)
return;
if (e.shiftKey) {
if (!userInfo.isFollowed) {
ppixiv.message.show("Not following this user");
return;
}
await Actions.unfollow(userId);
return;
}
let followPrivately = null;
if (e.altKey)
followPrivately = true;
if (userInfo.isFollowed) {
ppixiv.message.show("Already following this user");
return;
}
await Actions.follow(userId, followPrivately);
})();
return true;
}
return false;
}
_handleKeyEvent(e) {
if (e.type !== "keydown")
return false;
if (e.altKey && e.key === "Enter") {
helpers.toggleFullscreen();
return true;
}
if (this._isZoomUiEnabled) {
if (e.code === "Digit0" && e.ctrlKey) {
e.preventDefault();
e.stopImmediatePropagation();
this._currentViewer.zoomToggle();
return;
}
const zoom = helpers.isZoomHotkey(e);
if (zoom != null) {
e.preventDefault();
e.stopImmediatePropagation();
this._handleZoomEvent(e, zoom < 0);
return true;
}
}
if (this._handleKeyEventForImage(e))
return true;
if (this._handleKeyEventForUser(e))
return true;
return false;
}
onwheel = (e) => {
if (this.touchpadMode)
return;
if (!this._isZoomUiEnabled)
return;
const userDropdown = this.avatarWidget.userDropdownWidget;
if (userDropdown) {
const targetWidget = Widget.fromNode(e.target);
if (targetWidget) {
if (!userDropdown.isAncestorOf(targetWidget)) {
e.preventDefault();
e.stopImmediatePropagation();
}
}
return;
}
if (!this.visible)
return;
if (e.target.closest(".popup-bookmark-tag-dropdown"))
return;
e.preventDefault();
e.stopImmediatePropagation();
const down = e.deltaY > 0;
this._handleZoomEvent(e, down);
};
_handleZoomEvent(e, down) {
e.preventDefault();
e.stopImmediatePropagation();
if (!this.hideTemporarily) {
this.hideTemporarily = true;
}
const keyboard = e instanceof KeyboardEvent;
const x = keyboard ? null : e.clientX;
const y = keyboard ? null : e.clientY;
this._currentViewer.zoomAdjust(down, { x, y });
this.refresh();
}
_setTemporaryIllust(mediaId) {
if (this._clickedMediaId === mediaId)
return;
this._clickedMediaId = mediaId;
this.getUserIdFromMediaId.id = this._clickedMediaId ?? this._mediaId;
this.refresh();
}
refresh() {
const mediaId = this._effectiveMediaId;
if (this.visible)
this.getMediaInfo.id = mediaId;
if (!this.visible && mediaId != null)
return;
const userId = this._effectiveUserId;
ppixiv.userCache.getUserInfo(userId, {
full: true
}).then((userInfo) => {
this._userInfo = userInfo;
});
this._updateAuthorName(mediaId);
helpers.html.setClass(
this._buttonFullscreen,
"selected",
helpers.isFullscreen()
);
for (const element of this.root.querySelectorAll(".button.requires-zoom"))
helpers.html.setClass(element, "enabled", this._isZoomUiEnabled);
if (this.visible) {
for (const widget of this.illustWidgets) {
if (widget.setMediaId)
widget.setMediaId(mediaId);
if (widget.setUserId)
widget.setUserId(userId);
const showingViewedImage = this._clickedMediaId == null;
widget.showPageNumber = showingViewedImage;
}
const folderButton = this.root.querySelector(".button-parent-folder");
const authorButton = this.root.querySelector(".avatar-widget-container");
const isLocal2 = helpers.mediaId.isLocal(this._folderIdForParent);
folderButton.hidden = !isLocal2;
authorButton.hidden = isLocal2;
helpers.html.setClass(
folderButton,
"enabled",
this._parentFolderId != null
);
this.querySelector("[data-bookmark-type=private]").hidden = isLocal2;
}
if (this._isZoomUiEnabled) {
helpers.html.setClass(
this.root.querySelector(".button-zoom"),
"selected",
this._currentViewer.getLockedZoom()
);
const zoomLevel = this._currentViewer.getZoomLevel();
for (const button of this.root.querySelectorAll(".button-zoom-level"))
helpers.html.setClass(
button,
"selected",
this._currentViewer.getLockedZoom() && button.dataset.level === zoomLevel
);
}
}
_clickedFullscreen = async (e) => {
e.preventDefault();
e.stopPropagation();
await helpers.toggleFullscreen();
this.refresh();
};
_clickedToggleZoom = (e) => {
e.preventDefault();
e.stopPropagation();
if (!this._isZoomUiEnabled)
return;
this._currentViewer.zoomToggle({ x: e.clientX, y: e.clientY });
this.refresh();
};
_clickedZoomLevel = (e) => {
e.preventDefault();
e.stopPropagation();
if (!this._isZoomUiEnabled)
return;
this._currentViewer.zoomSetLevel(e.currentTarget.dataset.level, {
x: e.clientX,
y: e.clientY
});
this.refresh();
};
get _folderIdForParent() {
if (this._effectiveMediaId != null)
return this._effectiveMediaId;
const dataSourceMediaId = this.dataSource?.uiInfo.mediaId;
if (helpers.mediaId.isLocal(dataSourceMediaId))
return dataSourceMediaId;
return null;
}
get _parentFolderId() {
const folderId = this._folderIdForParent;
const isLocal2 = helpers.mediaId.isLocal(folderId);
if (!isLocal2)
return null;
let parentFolderId = LocalAPI.getParentFolder(folderId);
const currentlyDisplayingId = LocalAPI.getLocalIdFromArgs(
helpers.args.location
);
if (parentFolderId === currentlyDisplayingId)
parentFolderId = LocalAPI.getParentFolder(parentFolderId);
return parentFolderId;
}
clicked_go_to_parent = (e) => {
e.preventDefault();
const parentFolderId = this._parentFolderId;
if (parentFolderId == null)
return;
const args = new helpers.args("/", ppixiv.plocation);
LocalAPI.getArgsForId(parentFolderId, args);
helpers.navigate(args.url);
};
};
var ImageInfoWidget = class extends IllustWidget {
constructor({ showTitle = false, ...options }) {
super({
...options,
template: \`
<div class="context-menu-image-info-widget">
<div class=title-text-block>
<span class=folder-block hidden>
<span class=folder-text></span>
<span class=slash">/</span>
</span>
<span class=title hidden></span>
</div>
<div class=page-count hidden></div>
<div class=image-info hidden></div>
<div class="post-age popup" hidden></div>
</div>
\`
});
this.showTitle = showTitle;
}
get neededData() {
const mangaPage = this.mangaPage;
if (mangaPage == null || mangaPage === 0)
return "partial";
return "full";
}
set showPageNumber(value) {
this._showPageNumber = value;
this.refresh();
}
refreshInternal({ mediaId, mediaInfo }) {
this.root.hidden = mediaInfo == null;
if (this.root.hidden)
return;
const setInfo = (query, text) => {
const node = this.root.querySelector(query);
node.innerText = text;
node.hidden = text === "";
};
const pageCount = mediaInfo.pageCount;
let pageText = this.dataSource.getPageTextForMediaId(mediaId);
if (pageText == null && pageCount > 1) {
const currentPage = this.mangaPage;
if (this._showPageNumber || currentPage > 0)
pageText = \`Page \${currentPage + 1}/\${pageCount}\`;
else
pageText = \`\${pageCount} pages\`;
}
setInfo(".page-count", pageText ?? "");
if (this.showTitle) {
setInfo(".title", mediaInfo.illustTitle);
const showFolder = helpers.mediaId.isLocal(this._mediaId);
this.root.querySelector(".folder-block").hidden = !showFolder;
if (showFolder) {
const { id } = helpers.mediaId.parse(this._mediaId);
this.root.querySelector(".folder-text").innerText = helpers.strings.getPathSuffix(id, 1, 1);
}
}
let info = "";
const { width, height } = ppixiv.mediaCache.getImageDimensions(
mediaInfo,
this._mediaId
);
if (width != null && height != null)
info += \`\${width}x\${height}\`;
setInfo(".image-info", info);
const secondsOld = (new Date() - new Date(mediaInfo.createDate)) / 1e3;
const age = helpers.strings.ageToString(secondsOld);
this.root.querySelector(".post-age").dataset.popup = helpers.strings.dateToString(mediaInfo.createDate);
setInfo(".post-age", age);
}
setDataSource(dataSource) {
if (this.dataSource === dataSource)
return;
this.dataSource = dataSource;
this.refresh();
}
};
/\x2f web/vview/misc/muting.js
var Muting = class extends EventTarget {
constructor() {
super();
this._mutedTags = [];
this._mutedUserIds = [];
this._syncMutesChannel = new BroadcastChannel("ppixiv:mutes-changed");
this._syncMutesChannel.addEventListener("message", this._receivedMessage);
}
get pixivMutedTags() {
return this._mutedTags;
}
get pixivMutedUserIds() {
return this._mutedUserIds;
}
setMutes({ pixivMutedTags, pixivMutedUserIds } = {}) {
if (pixivMutedTags == null && pixivMutedUserIds == null)
return;
if (pixivMutedTags != null)
this._mutedTags = pixivMutedTags;
if (pixivMutedUserIds != null)
this._mutedUserIds = pixivMutedUserIds;
this._storeMutes();
this._fireMutesChanged();
}
get extraMutes() {
return ppixiv.settings.get("extraMutes");
}
set extraMutes(mutedUsers) {
ppixiv.settings.set("extraMutes", mutedUsers);
this._fireMutesChanged();
}
get _extraMutedTags() {
let tags = [];
for (let mute of this.extraMutes)
if (mute.type == "tag")
tags.push(mute.value);
return tags;
}
_fireMutesChanged() {
if (this.pixivMutedTags == null || this.pixivMutedUserIds == null)
return;
this.dispatchEvent(new Event("mutes-changed"));
this._broadcastMutes();
}
_broadcastMutes() {
if (this._handlingBroadcastMutes)
return;
this._syncMutesChannel.postMessage({
pixivMutedTags: this.pixivMutedTags,
pixivMutedUserIds: this.pixivMutedUserIds
});
}
_receivedMessage = (e) => {
let data = e.data;
if (this._handlingBroadcastMutes) {
console.error("recursive");
return;
}
if (JSON.stringify(this.pixivMutedTags) == JSON.stringify(data.pixivMutedTags) && JSON.stringify(this.pixivMutedUserIds) == JSON.stringify(data.pixivMutedUserIds))
return;
this._handlingBroadcastMutes = true;
try {
this.setMutes({ pixivMutedTags: data.pixivMutedTags, pixivMutedUserIds: data.pixivMutedUserIds });
} finally {
this._handlingBroadcastMutes = false;
}
};
isUserIdMuted(userId) {
if (this._mutedUserIds.indexOf(userId) != -1)
return true;
for (let { value: mutedUserId } of this.extraMutes) {
if (userId == mutedUserId)
return true;
}
return false;
}
async unmuteUserId(userId) {
this.removeExtraMute(userId, { type: "user" });
if (this._mutedUserIds.indexOf(userId) != -1)
await this.removePixivMute(userId, { type: "user" });
}
anyTagMuted(tagList) {
let _extraMutedTags = this._extraMutedTags;
for (let tag of tagList) {
if (tag.tag)
tag = tag.tag;
if (this._mutedTags.indexOf(tag) != -1 || _extraMutedTags.indexOf(tag) != -1)
return tag;
}
return null;
}
get _canAddPixivMutes() {
let total_mutes = this.pixivMutedTags.length + this.pixivMutedUserIds.length;
return ppixiv.pixivInfo.premium || total_mutes == 0;
}
_storeMutes() {
if (!ppixiv.mobile)
return;
ppixiv.settings.set("cached_mutes", {
tags: this._mutedTags,
userIds: this._mutedUserIds
});
}
loadCachedMutes() {
if (!ppixiv.mobile)
return;
let cachedMutes = ppixiv.settings.get("cached_mutes");
if (cachedMutes == null) {
console.log("No cached mutes to load");
return;
}
let { tags, userIds } = cachedMutes;
this._mutedTags = tags;
this._mutedUserIds = userIds;
}
async fetchMutes() {
let data = await helpers.pixivRequest.get(\`/touch/ajax/user/self/status?lang=en\`);
if (data.error) {
console.log("Error loading user info:", data.message);
return;
}
let mutes = data.body.user_status.mutes;
let pixivMutedTags = [];
for (let [tag, info] of Object.entries(mutes.tags)) {
if (info.enabled)
pixivMutedTags.push(tag);
}
let pixivMutedUserIds = [];
for (let [userId, info] of Object.entries(mutes.users)) {
if (info.enabled)
pixivMutedUserIds.push(userId);
}
this.setMutes({ pixivMutedTags, pixivMutedUserIds });
}
async addMute(value, label, { type }) {
if (ppixiv.pixivInfo.premium) {
await this.addPixivMute(value, { type });
} else {
if (type == "user" && label == null) {
let user_data = await ppixiv.userCache.getUserInfo(value);
label = user_data.name;
}
await this.addExtraMute(value, label, { type });
}
}
async addPixivMute(value, { type }) {
console.log(\`Adding \${value} to the Pixiv \${type} mute list\`);
if (!this._canAddPixivMutes) {
ppixiv.message.show("The Pixiv mute list is full.");
return;
}
let muteList = type == "tag" ? "pixivMutedTags" : "pixivMutedUserIds";
let mutes = this[muteList];
if (mutes.indexOf(value) != -1)
return;
let label = value;
if (type == "user")
label = (await ppixiv.userCache.getUserInfo(value)).name;
let result = await helpers.pixivRequest.rpcPost("/ajax/mute/items/add", {
context: "illust",
type,
value
});
if (result.error) {
ppixiv.message.show(result.message);
return;
}
mutes.push(value);
if (type == "tag")
mutes.sort();
let update = {};
update[muteList] = mutes;
this.setMutes(update);
ppixiv.message.show(\`Muted the \${type} \${label}\`);
}
async removePixivMute(value, { type }) {
console.log(\`Removing \${value} from the Pixiv muted \${type} list\`);
let label = value;
if (type == "user")
label = (await ppixiv.userCache.getUserInfo(value)).name;
let result = await helpers.pixivRequest.rpcPost("/ajax/mute/items/delete", {
context: "illust",
type,
value
});
if (result.error) {
ppixiv.message.show(result.message);
return;
}
let muteList = type == "tag" ? "pixivMutedTags" : "pixivMutedUserIds";
let mutes = this[muteList];
let idx = mutes.indexOf(value);
if (idx != -1)
mutes.splice(idx, 1);
let update = {};
update[muteList] = mutes;
this.setMutes(update);
ppixiv.message.show(\`Unmuted the \${type} \${label}\`);
}
async addExtraMute(value, label, { type }) {
console.log(\`Adding \${value} (\${label}) to the extra muted \${type} list\`);
let mutes = this.extraMutes;
for (let { value: mutedValue, type: mutedType } of mutes)
if (value == mutedValue && type == mutedType) {
console.log("Item is already muted");
return;
}
mutes.push({
type,
value,
label
});
mutes.sort((lhs, rhs) => {
return lhs.label.localeCompare(rhs.label);
});
this.extraMutes = mutes;
ppixiv.message.show(\`Muted the \${type} \${label}\`);
}
async removeExtraMute(value, { type }) {
console.log(\`Removing \${value} from the extra muted \${type} list\`);
let mutes = this.extraMutes;
for (let idx = 0; idx < mutes.length; ++idx) {
let mute = mutes[idx];
if (mute.type == type && mute.value == value) {
ppixiv.message.show(\`Unmuted the \${mute.type} \${mute.label}\`);
mutes.splice(idx, 1);
break;
}
}
this.extraMutes = mutes;
}
};
/\x2f web/vview/misc/settings.js
var Settings = class extends EventTarget {
constructor() {
super();
this.stickySettings = {};
this.sessionSettings = {};
this.defaults = {};
this.cache = {};
this.noValue = new Object();
this.configure("zoom-mode", { sticky: true });
this.configure("zoom-level", { sticky: true });
this.configure("linked_tabs", { session: true });
this.configure("linked_tabs_enabled", {
session: true,
defaultValue: true
});
this.configure("volume", { defaultValue: 1 });
this.configure("view_mode", { defaultValue: "illust" });
this.configure("image_editing", { session: true });
this.configure("image_editing_mode", { session: true });
this.configure("inpaint_create_lines", { session: true });
this.configure("slideshow_duration", { defaultValue: 15 });
this.configure("auto_pan", { defaultValue: ppixiv.mobile });
this.configure("auto_pan_duration", { defaultValue: 3 });
this.configure("slideshow_default", { defaultValue: "pan" });
this.configure("upscaling", { defaultValue: false });
this.configure("extraMutes", { defaultValue: [] });
this.configure("slideshow_skips_manga", { defaultValue: false });
this.configure("pixiv_cdn", { defaultValue: "pixiv" });
this.configure("preload_manga", {
defaultValue: ppixiv.mobile ? "thumbnails" : "full"
});
this.configure("manga_page_indicator", { defaultValue: false });
this.configure("collapse_autocomplete", { defaultValue: true });
this.configure("show_ai_icon", { defaultValue: true });
this.configure("use_original_download_filename", { defaultValue: true });
this.configure("thumbnail_style", {
defaultValue: helpers.other.isPhone() ? "square" : "aspect"
});
this.configure("expand_manga_thumbnails", { defaultValue: false });
this.configure("slideshow_framerate", { defaultValue: 60 });
this.configure("animations_enabled", { defaultValue: true });
this.configure("image_size_limit", {
defaultValue: ppixiv.mobile ? 4e3 * 4e3 : null
});
this.configure("translation_api_url", {
defaultValue: "https:/\x2fapi.cotrans.touhou.ai"
});
this.configure("translation_low_res", { defaultValue: false });
this.configure("translation_size", { defaultValue: "M" });
this.configure("translation_translator", { defaultValue: "deepl" });
this.configure("translation_direction", { defaultValue: "auto" });
this.configure("translation_language", { defaultValue: "ENG" });
this.configure("touchpad_mode", { defaultValue: true });
this.migrate();
}
configure(key, { sticky = false, session = false, defaultValue = null }) {
if (sticky) {
if (this.stickySettings[key] === void 0)
this.stickySettings[key] = null;
}
if (session)
this.sessionSettings[key] = true;
if (defaultValue != null)
this.defaults[key] = defaultValue;
}
_getStorageForKey(key) {
if (this.sessionSettings[key])
return sessionStorage;
else
return localStorage;
}
async _queueClearCache() {
if (this._clearCacheQueued || Object.keys(this.cache).length == 0)
return;
this._clearCacheQueued = true;
try {
await helpers.other.sleep(0);
this.cache = {};
} finally {
this._clearCacheQueued = false;
}
}
_cacheValue(key, value) {
this.cache[key] = value;
this._queueClearCache();
}
_getFromStorage(key, defaultValue) {
if (key in this.cache) {
let value = this.cache[key];
if (value === this.noValue)
return defaultValue;
else
return value;
}
let storage = this._getStorageForKey(key);
let settingKey = "_ppixiv_" + key;
if (!(settingKey in storage)) {
this._cacheValue(key, this.noValue);
return defaultValue;
}
let result = storage[settingKey];
try {
let value = JSON.parse(result);
this._cacheValue(key, value);
return value;
} catch (e) {
console.warn(e);
console.log("Removing invalid setting:", result);
delete storage.storage_key;
return defaultValue;
}
}
get(key, defaultValue) {
if (key in this.defaults)
defaultValue = this.defaults[key];
if (this.stickySettings[key])
return this.stickySettings[key];
let result = this._getFromStorage(key, defaultValue);
if (this.stickySettings[key] !== void 0)
this.stickySettings[key] = result;
return result;
}
migrate() {
}
set(key, value) {
let storage = this._getStorageForKey(key);
if (value === void 0)
throw "Key can't be set to undefined: " + key;
if (this.stickySettings[key] !== void 0)
this.stickySettings[key] = value;
let settingKey = "_ppixiv_" + key;
storage[settingKey] = JSON.stringify(value);
this._cacheValue(key, value);
this.dispatchEvent(new Event(key));
this.dispatchEvent(new Event("all"));
}
adjustZoom(setting, down) {
let value = this.get(setting);
if (typeof value != "number" || isNaN(value))
value = 4;
value += down ? -1 : 1;
value = helpers.math.clamp(value, 0, 7);
this.sliderValue = value;
this.value = this.sliderValue;
this.set(setting, value);
}
};
/\x2f web/vview/widgets/message-widget.js
var MessageWidget = class extends Widget {
constructor(options) {
super({
...options,
template: \`
<div class=hover-message>
<div class=message></div>
</div>\`
});
this.timer = null;
window.addEventListener("screenchanged", (e) => this.hide(), this._signal);
}
show(message) {
console.assert(message != null);
console.log(message);
this.clearTimer();
this.root.querySelector(".message").innerHTML = message;
this.root.classList.add("show");
this.root.classList.remove("centered");
this.timer = realSetTimeout(() => {
this.root.classList.remove("show");
}, 3e3);
}
clearTimer() {
if (this.timer != null) {
realClearTimeout(this.timer);
this.timer = null;
}
}
hide() {
this.clearTimer();
this.root.classList.remove("show");
}
};
/\x2f web/vview/misc/media-cache-mappings.js
var MediaCacheMappings = class {
static remapPartialMediaInfo(mediaInfo, source) {
let remappedMediaInfo = null;
if (source == "normal") {
if (!("title" in mediaInfo)) {
console.warn("Thumbnail info is missing key: title");
} else {
mediaInfo.illustTitle = mediaInfo.title;
delete mediaInfo.title;
}
let thumbnailInfoMap = this._thumbnailInfoMapRanking;
remappedMediaInfo = {};
for (let pair of thumbnailInfoMap) {
let key = pair[1];
if (!(key in mediaInfo)) {
console.warn("Thumbnail info is missing key:", key);
continue;
}
remappedMediaInfo[key] = mediaInfo[key];
}
if (!("bookmarkData" in mediaInfo))
console.warn("Thumbnail info is missing key: bookmarkData");
else {
remappedMediaInfo.bookmarkData = mediaInfo.bookmarkData;
if (remappedMediaInfo.bookmarkData != null)
delete remappedMediaInfo.bookmarkData.bookmarkId;
}
} else if (source == "illust_list" || source == "rankings") {
let thumbnailInfoMap = source == "illust_list" ? this._thumbnailInfoMapIllustList : this._thumbnailInfoMapRanking;
remappedMediaInfo = {};
for (let pair of thumbnailInfoMap) {
let fromKey = pair[0];
let toKey = pair[1];
if (fromKey == null) {
remappedMediaInfo[toKey] = null;
continue;
}
if (!(fromKey in mediaInfo)) {
console.warn("Thumbnail info is missing key:", fromKey);
continue;
}
let value = mediaInfo[fromKey];
remappedMediaInfo[toKey] = value;
}
remappedMediaInfo.id = "" + remappedMediaInfo.id;
remappedMediaInfo.userId = "" + remappedMediaInfo.userId;
remappedMediaInfo.bookmarkData = null;
if (!("is_bookmarked" in mediaInfo))
console.warn("Thumbnail info is missing key: is_bookmarked");
if (mediaInfo.is_bookmarked) {
remappedMediaInfo.bookmarkData = {
private: mediaInfo.bookmark_illust_restrict == 1
};
}
remappedMediaInfo.illustType = parseInt(remappedMediaInfo.illustType);
if (source == "rankings") {
let date = new Date(remappedMediaInfo.createDate * 1e3);
remappedMediaInfo.createDate = date.toISOString();
} else if (source == "illust_list") {
remappedMediaInfo.createDate = new Date(0).toISOString();
}
} else if (source == "internal") {
remappedMediaInfo = mediaInfo;
} else
throw "Unrecognized source: " + source;
if (source != "internal") {
for (let key of ["pageCount", "width", "height"]) {
if (remappedMediaInfo[key] != null)
remappedMediaInfo[key] = parseInt(remappedMediaInfo[key]);
}
remappedMediaInfo.url = helpers.pixiv.getHighResThumbnailUrl(remappedMediaInfo.url);
remappedMediaInfo.previewUrls = [];
for (let page = 0; page < remappedMediaInfo.pageCount; ++page) {
let url = helpers.pixiv.getHighResThumbnailUrl(remappedMediaInfo.url, page);
remappedMediaInfo.previewUrls.push(url);
}
remappedMediaInfo.tagList = remappedMediaInfo.tags;
delete remappedMediaInfo.tags;
remappedMediaInfo.illustId = remappedMediaInfo.id;
remappedMediaInfo.mediaId = helpers.mediaId.fromIllustId(remappedMediaInfo.illustId);
delete remappedMediaInfo.id;
}
let profileImageUrl = null;
if (remappedMediaInfo.profileImageUrl) {
profileImageUrl = remappedMediaInfo.profileImageUrl;
profileImageUrl = profileImageUrl.replace("_50.", "_170."), delete remappedMediaInfo.profileImageUrl;
}
return { remappedMediaInfo, profileImageUrl };
}
};
__publicField(MediaCacheMappings, "_thumbnailInfoMapIllustList", [
["illust_id", "id"],
["url", "url"],
["tags", "tags"],
["illust_user_id", "userId"],
["illust_width", "width"],
["illust_height", "height"],
["illust_type", "illustType"],
["illust_page_count", "pageCount"],
["illust_title", "illustTitle"],
["user_profile_img", "profileImageUrl"],
["user_name", "userName"],
[null, "createDate"],
[null, "aiType"]
]);
__publicField(MediaCacheMappings, "_thumbnailInfoMapRanking", [
["illust_id", "id"],
["url", "url"],
["tags", "tags"],
["user_id", "userId"],
["width", "width"],
["height", "height"],
["illust_type", "illustType"],
["illust_page_count", "pageCount"],
["title", "illustTitle"],
["profile_img", "profileImageUrl"],
["user_name", "userName"],
["illust_upload_timestamp", "createDate"],
["aiType", "aiType"]
]);
/\x2f web/vview/misc/media-cache.js
var MediaCache = class extends EventTarget {
constructor() {
super();
this._mediaInfo = {};
this._nonexistantMediaIds = {};
this._mediaInfoLoadsFull = {};
this._mediaInfoLoadsPartial = {};
this.userProfileUrls = {};
ppixiv.settings.addEventListener("pixiv_cdn", () => this._updatePixivURLs());
MediaInfoEvents.addEventListener("mediamodified", (e) => {
let event = new Event("mediamodified");
event.mediaId = e.mediaId;
this.dispatchEvent(event);
});
}
async getMediaInfo(mediaId, { full = true, safe = true } = {}) {
let mediaInfo = await this._getMediaInfoInner(mediaId, { full });
if (mediaInfo != null && !full && safe)
mediaInfo = mediaInfo.partialInfo;
return mediaInfo;
}
_getMediaInfoInner(mediaId, { full = true } = {}) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
if (mediaId == null)
return null;
if (mediaId in this._nonexistantMediaIds)
return null;
if (this._mediaInfo[mediaId] != null && (!full || this._mediaInfo[mediaId].full))
return Promise.resolve(this._mediaInfo[mediaId]);
if (this._mediaInfoLoadsFull[mediaId] != null)
return this._mediaInfoLoadsFull[mediaId].then(() => this._mediaInfo[mediaId]);
if (!full && this._mediaInfoLoadsPartial[mediaId] != null)
return this._mediaInfoLoadsPartial[mediaId].then(() => this._mediaInfo[mediaId]);
let loadPromise2 = this._loadMediaInfo(mediaId);
this._startedLoadingMediaInfoFull(mediaId, loadPromise2);
return loadPromise2;
}
getMediaInfoSync(mediaId, { full = true, safe = true } = {}) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
let mediaInfo = this._mediaInfo[mediaId];
if (full && !mediaInfo?.full)
return null;
if (mediaInfo && !full && safe)
mediaInfo = mediaInfo.partialInfo;
return mediaInfo;
}
getMediaLoadError(mediaId) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
return this._nonexistantMediaIds[mediaId];
}
async refreshMediaInfo(mediaId, { refreshFromDisk = false } = {}) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
await this._loadMediaInfo(mediaId, { refreshFromDisk });
}
addPixivFullMediaInfo(mediaInfo) {
let mediaId = helpers.mediaId.fromIllustId(mediaInfo.id);
let loadPromise2 = this._loadMediaInfo(mediaId, { mediaInfo });
this._startedLoadingMediaInfoFull(mediaId, loadPromise2);
return loadPromise2;
}
_startedLoadingMediaInfoFull(mediaId, loadPromise2) {
this._mediaInfoLoadsFull[mediaId] = loadPromise2;
this._mediaInfoLoadsFull[mediaId].finally(() => {
if (this._mediaInfoLoadsFull[mediaId] === loadPromise2)
delete this._mediaInfoLoadsFull[mediaId];
});
}
_startedLoadingMediaInfoPartial(mediaId, loadPromise2) {
this._mediaInfoLoadsPartial[mediaId] = loadPromise2;
this._mediaInfoLoadsPartial[mediaId].finally(() => {
if (this._mediaInfoLoadsPartial[mediaId] === loadPromise2)
delete this._mediaInfoLoadsPartial[mediaId];
});
}
async _loadMediaInfo(mediaId, { mediaInfo = null, refreshFromDisk = false } = {}) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
let [illustId] = helpers.mediaId.toIllustIdAndPage(mediaId);
delete this._nonexistantMediaIds[mediaId];
if (helpers.mediaId.isLocal(mediaId))
return await this._loadLocalImageData(mediaId, { refreshFromDisk });
let mangaPromise = null;
let ugoiraPromise = null;
let startLoading = (illustType, pageCount) => {
if (pageCount != null && pageCount > 1 && mangaPromise == null && mediaInfo?.mangaPages == null)
mangaPromise = helpers.pixivRequest.get(\`/ajax/illust/\${illustId}/pages\`, {});
if (illustType == 2 && ugoiraPromise == null && (mediaInfo == null || mediaInfo.ugoiraMetadata == null))
ugoiraPromise = helpers.pixivRequest.get(\`/ajax/illust/\${illustId}/ugoira_meta\`);
};
let partialInfo = this._mediaInfo[mediaId];
if (partialInfo != null)
startLoading(partialInfo.illustType, partialInfo.pageCount);
if (mediaInfo == null) {
let illustResultPromise = helpers.pixivRequest.get(\`/ajax/illust/\${illustId}\`, {});
let illustResult = await illustResultPromise;
if (illustResult == null || illustResult.error) {
let message = illustResult?.message || "Error loading illustration";
console.log(\`Error loading illust \${illustId}; \${message}\`);
this._nonexistantMediaIds[mediaId] = message;
return null;
}
mediaInfo = illustResult.body;
}
ppixiv.tagTranslations.addTranslations(mediaInfo.tags.tags);
let extraData = await ppixiv.extraImageData.loadAllPagesForIllust(illustId);
mediaInfo.extraData = extraData;
startLoading(mediaInfo.illustType, mediaInfo.pageCount);
mediaInfo.previewUrls = [];
for (let page = 0; page < mediaInfo.pageCount; ++page) {
let url = helpers.pixiv.getHighResThumbnailUrl(mediaInfo.urls.small, page);
mediaInfo.previewUrls.push(url);
}
mediaInfo.tagList = [];
for (let tag of mediaInfo.tags.tags)
mediaInfo.tagList.push(tag.tag);
if (mangaPromise != null) {
let mangaInfo = await mangaPromise;
mediaInfo.mangaPages = mangaInfo.body;
}
if (ugoiraPromise != null) {
let ugoiraResult = await ugoiraPromise;
mediaInfo.ugoiraMetadata = ugoiraResult.body;
} else
mediaInfo.ugoiraMetadata = null;
this._updateMediaInfoUrls(mediaInfo);
if (mediaInfo.pageCount == 1) {
mediaInfo.mangaPages = [{
width: mediaInfo.width,
height: mediaInfo.height,
urls: {
original: mediaInfo.urls.original,
regular: mediaInfo.urls.regular,
small: mediaInfo.urls.small
}
}];
}
if (mediaInfo.userIllusts) {
for (let userIllustData of Object.values(mediaInfo.userIllusts)) {
if (userIllustData?.profileImageUrl == null)
continue;
let { profileImageUrl } = MediaCacheMappings.remapPartialMediaInfo(userIllustData, "normal");
if (profileImageUrl)
this.cacheProfilePictureUrl(mediaInfo.userId, profileImageUrl);
}
}
mediaInfo.full = true;
mediaInfo.mediaId = mediaId;
delete mediaInfo.id;
delete mediaInfo.userIllusts;
ppixiv.guessImageUrl.addInfo(mediaInfo);
return this.addFullMediaInfo(mediaInfo);
}
_updatePixivURLs() {
for (let mediaInfo of Object.values(this._mediaInfo))
this._updateMediaInfoUrls(mediaInfo);
for (let mediaId of Object.keys(this._mediaInfo))
MediaInfo.callMediaInfoModifiedCallbacks(mediaId);
}
_updateMediaInfoUrls(mediaInfo) {
if (mediaInfo.urls) {
for (let [key, url] of Object.entries(mediaInfo.urls)) {
url = new URL(url);
mediaInfo.urls[key] = helpers.pixiv.adjustImageUrlHostname(url).toString();
}
}
if (mediaInfo.previewUrls) {
for (let page = 0; page < mediaInfo.previewUrls.length; ++page) {
let url = mediaInfo.previewUrls[page];
mediaInfo.previewUrls[page] = helpers.pixiv.adjustImageUrlHostname(url).toString();
}
}
if (mediaInfo.mangaPages) {
for (let page of mediaInfo.mangaPages) {
for (let [key, url] of Object.entries(page.urls)) {
url = helpers.pixiv.adjustImageUrlHostname(url);
page.urls[key] = url.toString();
}
}
}
if (mediaInfo.ugoiraMetadata) {
let url = new URL(mediaInfo.ugoiraMetadata.originalSrc);
url = helpers.pixiv.adjustImageUrlHostname(url);
mediaInfo.ugoiraMetadata.originalSrc = url.toString();
}
}
async batchGetMediaInfoPartial(mediaIds, { force = false, userId = null } = {}) {
let promises = [];
let neededMediaIds = [];
let localMediaIds = [];
for (let mediaId of mediaIds) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
if (!force && this._mediaInfo[mediaId] != null)
continue;
if (!force && this._nonexistantMediaIds[mediaId])
continue;
let existingLoad = this._mediaInfoLoadsFull[mediaId] ?? this._mediaInfoLoadsPartial[mediaId];
if (existingLoad) {
promises.push(existingLoad);
continue;
}
let { type } = helpers.mediaId.parse(mediaId);
if (helpers.mediaId.isLocal(mediaId))
localMediaIds.push(mediaId);
else if (type == "illust")
neededMediaIds.push(mediaId);
}
if (localMediaIds.length) {
let loadPromise2 = this._loadLocalMediaIds(localMediaIds);
for (let mediaId of mediaIds)
this._startedLoadingMediaInfoFull(mediaId, loadPromise2);
promises.push(loadPromise2);
}
if (neededMediaIds.length) {
let loadPromise2 = this._doBatchGetMediaInfo(neededMediaIds, { userId });
for (let mediaId of mediaIds)
this._startedLoadingMediaInfoPartial(mediaId, loadPromise2);
promises.push(loadPromise2);
}
await Promise.all(promises);
}
async _doBatchGetMediaInfo(mediaIds, { userId = null } = {}) {
if (mediaIds.length == 0)
return;
let illustIds = [];
for (let mediaId of mediaIds) {
if (helpers.mediaId.parse(mediaId).type != "illust")
continue;
let [illustId] = helpers.mediaId.toIllustIdAndPage(mediaId);
illustIds.push(illustId);
}
if (userId != null) {
let url = \`/ajax/user/\${userId}/profile/illusts\`;
let result = await helpers.pixivRequest.get(url, {
"ids[]": illustIds,
work_category: "illustManga",
is_first_page: "0"
});
let illusts = Object.values(result.body.works);
await this.addMediaInfosPartial(illusts, "normal");
} else {
let result = await helpers.pixivRequest.get("/rpc/illust_list.php", {
illust_ids: illustIds.join(","),
page: "discover",
exclude_muted_illusts: 1
});
await this.addMediaInfosPartial(result, "illust_list");
}
for (let mediaId of mediaIds) {
if (this._mediaInfo[mediaId] == null && this._nonexistantMediaIds[mediaId] == null)
this._nonexistantMediaIds[mediaId] = "Illustration doesn't exist";
}
}
addMediaInfosPartial = async (searchResult, source) => {
if (searchResult.error)
return [];
searchResult = searchResult.filter((item) => !item.isAdContainer);
let allThumbInfo = [];
let mediaIds = [];
for (let thumbInfo of searchResult) {
let { remappedMediaInfo, profileImageUrl } = MediaCacheMappings.remapPartialMediaInfo(thumbInfo, source);
mediaIds.push(remappedMediaInfo.mediaId);
if (profileImageUrl)
this.cacheProfilePictureUrl(remappedMediaInfo.userId, profileImageUrl);
allThumbInfo.push(remappedMediaInfo);
}
let illustIds = allThumbInfo.map((info) => info.illustId);
let extraData = await ppixiv.extraImageData.batchLoadAllPagesForIllust(illustIds);
for (let mediaInfo of allThumbInfo) {
mediaInfo.extraData = extraData[mediaInfo.illustId] || {};
mediaInfo.full = false;
this._updateMediaInfoUrls(mediaInfo);
this.addFullMediaInfo(mediaInfo);
}
return mediaIds;
};
async _loadLocalImageData(mediaId, { refreshFromDisk } = {}) {
let mediaInfo = await LocalAPI.loadMediaInfo(mediaId, { refreshFromDisk });
if (!mediaInfo.success) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
this._nonexistantMediaIds[mediaId] = mediaInfo.reason;
return null;
}
return this.addFullMediaInfo(mediaInfo.illust);
}
addFullMediaInfo(mediaInfo) {
mediaInfo = MediaInfo.createFrom({ mediaInfo });
let { mediaId } = mediaInfo;
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
if (mediaId in this._mediaInfo) {
this._mediaInfo[mediaId].updateInfo(mediaInfo);
} else
this._mediaInfo[mediaId] = mediaInfo;
MediaInfo.callMediaInfoModifiedCallbacks(mediaId);
return mediaInfo;
}
areAllMediaIdsLoadedOrLoading(mediaIds) {
for (let mediaId of mediaIds) {
if (!this.isMediaIdLoadedOrLoading(mediaId))
return false;
}
return true;
}
isMediaIdLoadedOrLoading(mediaId) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
return this._mediaInfo[mediaId] != null || this._mediaInfoLoadsFull[mediaId] || this._mediaInfoLoadsPartial[mediaId];
}
async saveExtraImageData(mediaId, edits) {
let [illustId] = helpers.mediaId.toIllustIdAndPage(mediaId);
let results = await ppixiv.extraImageData.loadMediaId([mediaId]);
let data = results[mediaId] ?? { illust_id: illustId };
for (let [key, value] of Object.entries(edits))
data[key] = value;
for (let [key, value] of Object.entries(data)) {
if (value == null)
delete data[key];
}
data.edited_at = Date.now() / 1e3;
if (Object.keys(data).length == 2)
await ppixiv.extraImageData.deleteMediaId(mediaId);
else
await ppixiv.extraImageData.updateMediaId(mediaId, data);
this.replaceExtraData(mediaId, data);
return data;
}
replaceExtraData(mediaId, data) {
let mediaInfo = this.getMediaInfoSync(mediaId, { full: false });
if (mediaInfo == null)
return;
mediaInfo.extraData[mediaId] = data;
MediaInfo.callMediaInfoModifiedCallbacks(mediaId);
}
getProfilePictureUrl(userId) {
let result = this.userProfileUrls[userId];
if (!result)
result = "https:/\x2fs.pximg.net/common/images/no_profile.png";
return result;
}
cacheProfilePictureUrl(userId, url) {
if (this.userProfileUrls[userId] == url)
return;
this.userProfileUrls[userId] = url;
helpers.other.preloadImages([url]);
}
getExtraData(mediaInfo, mediaId, page = null) {
if (mediaInfo == null)
return {};
if (page != null)
mediaId = helpers.mediaId.getMediaIdForPage(mediaId, page);
return mediaInfo.extraData[mediaId] ?? {};
}
getImageDimensions(mediaInfo, mediaId = null, page = null, {} = {}) {
if (mediaInfo == null)
return { width: 1, height: 1 };
let pageInfo = mediaInfo;
if (!helpers.mediaId.isLocal(mediaInfo.mediaId)) {
if (page == null) {
if (mediaId == null)
throw new Error("At least one of mediaId or page must be specified");
page = helpers.mediaId.toIllustIdAndPage(mediaId)[1];
}
if (page > 0) {
if (!mediaInfo.full)
return { width: pageInfo.width, height: pageInfo.height };
pageInfo = mediaInfo.mangaPages[page];
}
}
return { width: pageInfo.width, height: pageInfo.height };
}
async _loadLocalMediaIds(mediaIds) {
if (mediaIds.length == 0)
return;
let result = await LocalAPI.localPostRequest(\`/api/illusts\`, {
ids: mediaIds
});
if (!result.success) {
console.error("Error reading IDs:", result.reason);
return;
}
for (let illust of result.results)
await this.addFullMediaInfo(illust);
}
async localSearch(path = "", { ...options } = {}) {
let result = await LocalAPI.localPostRequest(\`/api/list/\${path}\`, {
...options
});
if (!result.success) {
console.error("Error reading directory:", result.reason);
return result;
}
for (let illust of result.results)
await this.addFullMediaInfo(illust);
return result;
}
};
/\x2f web/vview/misc/user-cache.js
var UserCache = class extends EventTarget {
constructor() {
super();
this._userData = {};
this._allUserFollowTags = null;
this._userFollowInfo = {};
this._userInfoLoads = {};
this._followInfoLoads = {};
this._userFollowTagsLoad = null;
this._userProfile = {};
this._userProfileLoads = {};
this._nonexistantUserIds = {};
this._userBoothUrls = {};
}
async getUserIdForMediaId(mediaId) {
if (mediaId == null)
return null;
let { type, id } = helpers.mediaId.parse(mediaId);
if (type == "user")
return id;
let mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, { full: false });
return mediaInfo?.userId;
}
callUserModifiedCallbacks(userId) {
console.log(\`User modified: \${userId}\`);
let event = new Event("usermodified");
event.userId = userId;
this.dispatchEvent(event);
}
getUserLoadError(userId) {
return this._nonexistantUserIds[userId];
}
async getUserInfo(userId, { full = false } = {}) {
return await this._getUserInfo(userId, full);
}
getUserInfoSync(userId, { full = false } = {}) {
let userInfo = this._userData[userId];
if (userInfo == null)
return null;
if (full && !userInfo.partial)
return null;
return userInfo;
}
_getUserInfo(userId, loadFullData) {
if (userId == null)
return null;
let baseMediaId = \`user:\${userId}\`;
if (baseMediaId in this._nonexistantUserIds)
return null;
if (this._userData[userId] != null) {
if (!loadFullData || this._userData[userId].partial) {
return new Promise((resolve) => {
resolve(this._userData[userId]);
});
}
}
if (this._userInfoLoads[userId] != null)
return this._userInfoLoads[userId];
this._userInfoLoads[userId] = this._loadUserInfo(userId);
this._userInfoLoads[userId].then(() => {
delete this._userInfoLoads[userId];
});
return this._userInfoLoads[userId];
}
async _loadUserInfo(userId) {
if (userId == -1)
return null;
let result = await helpers.pixivRequest.get(\`/ajax/user/\${userId}\`, { full: 1 });
if (result == null || result.error) {
let message = result?.message || "Error loading user";
console.log(\`Error loading user \${userId}: \${message}\`);
this._nonexistantUserIds[\`user:\${userId}\`] = message;
return null;
}
return this._loadedUserInfo(result);
}
addUserData(userData) {
this._loadedUserInfo({
body: userData
});
}
_loadedUserInfo = (userResult) => {
if (userResult.error)
return;
let userData = userResult.body;
userData = this._checkUserData(userData);
let userId = userData.userId;
if (this._userData[userId] == null)
this._userData[userId] = userData;
else {
let oldUserData = this._userData[userId];
for (let key of Object.keys(oldUserData))
delete oldUserData[key];
for (let key of Object.keys(userData))
oldUserData[key] = userData[key];
}
return userData;
};
_checkUserData(userData) {
let fullKeys = [
"userId",
"background",
"imageBig",
"isFollowed",
"isMypixiv",
"name",
"partial",
"social",
"commentHtml"
];
let partialKeys = [
"userId",
"isFollowed",
"name",
"imageBig",
"partial"
];
let expectedKeys = userData.partial ? fullKeys : partialKeys;
let remappedUserData = {};
for (let key of expectedKeys) {
if (!(key in userData)) {
console.warn("User info is missing key:", key);
continue;
}
remappedUserData[key] = userData[key];
}
return remappedUserData;
}
getUserProfile(userId) {
if (userId == null)
return null;
if (this._userProfile[userId])
return this._userProfile[userId];
let baseMediaId = \`user:\${userId}\`;
if (baseMediaId in this._nonexistantUserIds)
return null;
if (this._userProfileLoads[userId] != null)
return this._userProfileLoads[userId];
this._userProfileLoads[userId] = this._loadUserProfile(userId);
this._userProfileLoads[userId].then(() => {
delete this._userProfileLoads[userId];
});
return this._userProfileLoads[userId];
}
getUserProfileSync(userId) {
return this._userProfile[userId];
}
async _loadUserProfile(userId) {
if (userId == -1)
return null;
let result = await helpers.pixivRequest.get(\`/ajax/user/\${userId}/profile/all\`);
if (result == null || result.error) {
let message = result?.message || "Error loading user";
console.log(\`Error loading user \${userId}: \${message}\`);
this._nonexistantUserIds[\`user:\${userId}\`] = message;
return null;
}
this._userProfile[userId] = result;
return result;
}
getUserBoothUrl(userId) {
if (userId in this._userBoothUrls)
return this._userBoothUrls[userId];
let promise = this._loadUserBoothUrl(userId);
promise.then((url) => this._userBoothUrls[userId] = url);
return promise;
}
async _loadUserBoothUrl(userId) {
let userProfile = await ppixiv.userCache.getUserProfile(userId);
if (!userProfile.body?.externalSiteWorksStatus?.booth)
return null;
let boothInfo = await helpers.pixivRequest.get("https:/\x2fapi.booth.pm/pixiv/shops/show.json", {
pixiv_user_id: userId,
adult: "exclude",
limit: 1
});
if (boothInfo.error)
return null;
return boothInfo.body.url;
}
async getUserFollowInfo(userId, { refresh = false } = {}) {
let userData = await this.getUserInfo(userId);
if (!userData.isFollowed) {
delete this._userFollowInfo[userId];
return null;
}
if (!refresh && this._userFollowInfo[userId])
return this._userFollowInfo[userId];
if (this._followInfoLoads[userId]) {
await this._followInfoLoads[userId];
return this._userFollowInfo[userId];
}
this._followInfoLoads[userId] = helpers.pixivRequest.get("/ajax/following/user/details", {
user_id: userId,
lang: "en"
});
let data = await this._followInfoLoads[userId];
this._followInfoLoads[userId] = null;
if (data.error) {
console.log(\`Couldn't request follow info for \${userId}\`);
return null;
}
let allTags = [];
let tags = /* @__PURE__ */ new Set();
for (let tagInfo of data.body.tags) {
allTags.push(tagInfo.name);
if (tagInfo.selected)
tags.add(tagInfo.name);
}
this._setCachedAllUserFollowTags(allTags);
this._userFollowInfo[userId] = {
tags,
followingPrivately: data.body.restrict == "1"
};
return this._userFollowInfo[userId];
}
getUserFollowInfoSync(userId) {
return this._userFollowInfo[userId];
}
async loadAllUserFollowTags({ refresh = false } = {}) {
if (!ppixiv.pixivInfo.premium)
return [];
if (!refresh && this._allUserFollowTags != null)
return this._allUserFollowTags;
if (this._userFollowTagsLoad) {
await this._userFollowTagsLoad;
return this._allUserFollowTags;
}
this._userFollowTagsLoad = helpers.pixivRequest.get(\`/ajax/user/\${ppixiv.pixivInfo.userId}/following\`, {
offset: 0,
limit: 1,
rest: "show"
});
let result = await this._userFollowTagsLoad;
this._userFollowTagsLoad = null;
if (result.error)
console.log("Error retrieving follow tags");
else
this._setCachedAllUserFollowTags(result.body.followUserTags);
return this._allUserFollowTags;
}
_setCachedAllUserFollowTags(tags) {
tags.sort();
let idx = tags.indexOf("null");
if (idx != -1)
tags.splice(idx, 1);
this._allUserFollowTags = tags;
}
addCachedUserFollowTags(tag) {
if (this._allUserFollowTags == null || this._allUserFollowTags.indexOf(tag) != -1)
return;
this._allUserFollowTags.push(tag);
this._allUserFollowTags.sort();
}
getAllUserFollowTagsSync() {
return this._allUserFollowTags;
}
updateCachedFollowInfo(userId, followed, followInfo) {
let userInfo = this.getUserInfoSync(userId);
if (userInfo == null)
return;
userInfo.isFollowed = followed;
if (!followed) {
delete this._userFollowInfo[userId];
} else {
this._userFollowInfo[userId] = followInfo;
}
this.callUserModifiedCallbacks(userId);
}
};
/\x2f web/vview/misc/extra-cache.js
var ExtraCache = class {
constructor() {
this._bookmarkedImageTags = {};
this._recentLikes = {};
this._quickUserData = {};
this._getMediaAspectRatioLoads = {};
this._mediaIdAspectRatio = {};
}
getLikedRecently(mediaId) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
return this._recentLikes[mediaId];
}
addLikedRecently(mediaId) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
this._recentLikes[mediaId] = true;
}
async loadBookmarkDetails(mediaId) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
let thumb = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (thumb && thumb.bookmarkData == null)
return [];
if (helpers.mediaId.isLocal(mediaId))
this._bookmarkedImageTags[mediaId] = thumb.bookmarkData.tags;
if (this._bookmarkedImageTags[mediaId])
return [...this._bookmarkedImageTags[mediaId]];
let [illustId] = helpers.mediaId.toIllustIdAndPage(mediaId);
let bookmarkPage = await helpers.pixivRequest.fetchDocument("/bookmark_add.php?type=illust&illust_id=" + illustId);
let tags = bookmarkPage.querySelector(".bookmark-detail-unit form input[name='tag']").value;
tags = tags.split(" ");
tags = tags.filter((value) => {
return value != "";
});
this._bookmarkedImageTags[mediaId] = tags;
return this._bookmarkedImageTags[mediaId];
}
getBookmarkDetailsSync(mediaId) {
if (helpers.mediaId.isLocal(mediaId)) {
let thumb = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (thumb && thumb.bookmarkData == null)
return [];
this._bookmarkedImageTags[mediaId] = thumb.bookmarkData.tags;
return this._bookmarkedImageTags[mediaId];
} else
return this._bookmarkedImageTags[mediaId];
}
updateCachedBookmarkTags(mediaId, tags) {
mediaId = helpers.mediaId.getMediaIdFirstPage(mediaId);
if (tags == null)
delete this._bookmarkedImageTags[mediaId];
else
this._bookmarkedImageTags[mediaId] = tags;
MediaInfo.callMediaInfoModifiedCallbacks(mediaId);
}
addQuickUserData(sourceData, source = "normal") {
let data = null;
if (source == "normal" || source == "following") {
data = {
userId: sourceData.userId,
userName: sourceData.userName,
profileImageUrl: sourceData.profileImageUrl
};
} else if (source == "recommendations") {
data = {
userId: sourceData.userId,
userName: sourceData.name,
profileImageUrl: sourceData.imageBig
};
} else if (source == "users_bookmarking_illust") {
data = {
userId: sourceData.user_id,
userName: sourceData.user_name,
profileImageUrl: sourceData.profile_img
};
} else
throw "Unknown source: " + source;
this._quickUserData[data.userId] = data;
}
getQuickUserData(userId) {
return this._quickUserData[userId];
}
getMediaAspectRatio(mediaId, { allowMediaInfoLoad = false } = {}) {
if (this._mediaIdAspectRatio[mediaId] != null)
return this._mediaIdAspectRatio[mediaId];
if (this._getMediaAspectRatioLoads[mediaId])
return this._getMediaAspectRatioLoads[mediaId];
let promise = this._getMediaAspectRatioInner(mediaId, { allowMediaInfoLoad });
this._getMediaAspectRatioLoads[mediaId] = promise;
promise.then((result) => {
this._mediaIdAspectRatio[mediaId] = result;
});
promise.finally(() => {
delete this._getMediaAspectRatioLoads[mediaId];
});
return promise;
}
getMediaAspectRatioSync(mediaId) {
return this._mediaIdAspectRatio[mediaId];
}
async _getMediaAspectRatioInner(mediaId, { allowMediaInfoLoad = false } = {}) {
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (mediaInfo == null) {
if (!allowMediaInfoLoad) {
console.error(\`getMediaResolution(\${mediaId}): media info wasn't loaded\`);
return null;
}
mediaInfo = await ppixiv.mediaCache.getMediaInfo(mediaId, { full: false });
}
if (helpers.mediaId.isLocal(mediaId))
return mediaInfo.width / mediaInfo.height;
let page = helpers.mediaId.parse(mediaId).page;
let url = mediaInfo.previewUrls[page];
let img = document.createElement("img");
img.src = url;
return await this.registerLoadingThumbnail(mediaId, img);
}
batchGetMediaAspectRatio(mediaIds) {
let aspectRatios = {};
let promises = [];
for (let mediaId of mediaIds) {
let aspectRatio = this.getMediaAspectRatioSync(mediaId);
if (aspectRatio != null)
aspectRatios[mediaId] = aspectRatio;
else
promises.push(this.getMediaAspectRatio(mediaId));
}
let promise = promises.length > 0 ? Promise.all(promises) : null;
return { aspectRatios, promise };
}
async registerLoadingThumbnail(mediaId, img) {
await helpers.other.waitForImageLoad(img);
let aspectRatio = img.naturalWidth / img.naturalHeight;
if (img.naturalHeight == 0)
aspectRatio = 0;
this._mediaIdAspectRatio[mediaId] = aspectRatio;
return aspectRatio;
}
};
/\x2f web/vview/util/hooks.js
var Hooks = null;
async function init() {
let app = ppixiv.app;
let url = localStorage.vviewHooksUrl;
if (!url)
return;
let exports = {
MenuOption: menu_option_exports,
LocalAPI: local_api_exports,
Helpers: helpers_exports
};
Hooks = await import(url);
await Hooks?.init?.({ app, exports });
}
/\x2f web/vview/misc/extra-image-data.js
var ExtraImageData = class {
constructor() {
if (ppixiv.native)
return;
this.db = new KeyStorage("ppixiv-image-data", { upgradeDb: this.upgradeDb });
}
upgradeDb = (e) => {
let db = e.target.result;
let store = db.createObjectStore("ppixiv-image-data");
store.createIndex("illust_id", "illust_id");
store.createIndex("edited_at", "edited_at");
};
async updateMediaId(mediaId, data) {
if (this.db == null)
return;
if (!this._requestedPersistentStorage && navigator.storage?.persist) {
this._requestedPersistentStorage = true;
navigator.storage.persist();
}
await this.db.set(mediaId, data);
}
async deleteMediaId(mediaId) {
if (this.db == null)
return;
await this.db.delete(mediaId);
}
async loadMediaId(mediaIds) {
if (this.db == null)
return {};
return await this.db.dbOp(async (db) => {
let store = this.db.getStore(db);
let promises = {};
for (let mediaId of mediaIds) {
let data = KeyStorage.asyncStoreGet(store, mediaId);
if (data)
promises[mediaId] = data;
}
return await helpers.other.awaitMap(promises);
}) ?? {};
}
async loadAllPagesForIllust(illustId) {
if (this.db == null)
return {};
return await this.db.dbOp(async (db) => {
let store = this.db.getStore(db);
let index = store.index("illust_id");
let query = IDBKeyRange.only(illustId);
let cursor = index.openCursor(query);
let results = {};
for await (let entry of cursor) {
let mediaId = entry.primaryKey;
results[mediaId] = entry.value;
}
return results;
}) ?? {};
}
async batchLoadAllPagesForIllust(illustIds) {
if (this.db == null)
return {};
return await this.db.dbOp(async (db) => {
let store = this.db.getStore(db);
let index = store.index("illust_id");
let promises = {};
for (let illustId of illustIds) {
let query = IDBKeyRange.only(illustId);
let cursor = index.openCursor(query);
promises[illustId] = (async () => {
let results = {};
for await (let entry of cursor) {
let mediaId = entry.primaryKey;
results[mediaId] = entry.value;
}
return results;
})();
}
return await helpers.other.awaitMap(promises);
}) ?? {};
}
async getAllEditedImages({ sort = "time" } = {}) {
console.assert(sort == "time" || sort == "id");
if (this.db == null)
return [];
return await this.db.dbOp(async (db) => {
let store = this.db.getStore(db);
let index = sort == "time" ? store.index("edited_at") : store;
let cursor = index.openKeyCursor(null, sort == "time" ? "prev" : "next");
let results = [];
for await (let entry of cursor) {
let mediaId = entry.primaryKey;
results.push(mediaId);
}
return results;
}) ?? [];
}
async export() {
if (this.db == null)
throw new Error("ExtraImageData is disabled");
let data = await this.db.dbOp(async (db) => {
let store = this.db.getStore(db);
let cursor = store.openCursor();
let results = [];
for await (let entry of cursor) {
results.push({
media_id: entry.key,
...entry.value
});
}
return results;
}) ?? [];
let exportedData = {
type: "ppixiv-image-data",
data
};
if (exportedData.data.length == 0) {
ppixiv.message.show("No edited images to export.");
return;
}
let json = JSON.stringify(exportedData, null, 4);
let blob = new Blob([json], { type: "application/json" });
helpers.saveBlob(blob, "ppixiv image edits.json");
}
async import() {
if (this.db == null)
throw new Error("ExtraImageData is disabled");
let files;
try {
files = await window.showOpenFilePicker({
multiple: false,
types: [{
description: "Exported image edits",
accept: {
"application/json": [".json"]
}
}]
});
} catch (e) {
return;
}
let file = await files[0].getFile();
let data = JSON.parse(await file.text());
if (data.type != "ppixiv-image-data") {
ppixiv.message.show(\`The file "\${file.name}" doesn't contain exported image edits.\`);
return;
}
let dataByMediaId = {};
for (let entry of data.data) {
let mediaId = entry.media_id;
delete entry.media_id;
dataByMediaId[mediaId] = entry;
}
console.log(\`Importing data:\`, data);
await this.db.multiSet(dataByMediaId);
for (let [mediaId, data2] of Object.entries(dataByMediaId))
ppixiv.mediaCache.replaceExtraData(mediaId, data2);
ppixiv.message.show(\`Imported edits for \${data.data.length} \${data.data.length == 1 ? "image" : "images"}.\`);
}
};
/\x2f web/vview/misc/guess-image-url.js
var GuessImageURL = class {
constructor() {
this.db = new KeyStorage("ppixiv-file-types", {
upgradeDb: this.upgradeDb
});
}
upgradeDb = (e) => {
let db = e.target.result;
let store = db.createObjectStore("ppixiv-file-types", {
keyPath: "illust_id_and_page"
});
store.createIndex("user_id_and_filetype", ["user_id", "page", "ext"]);
};
addInfo(imageData) {
const pages = [];
for (let page = 0; page < imageData.pageCount; ++page) {
const illustId = imageData.illustId;
const mediaId = helpers.mediaId.fromIllustId(imageData.illustId, page);
const url = imageData?.mangaPages?.[page].urls.original;
const parts = url.split(".");
const ext = parts[parts.length - 1];
pages.push({
illust_id_and_page: mediaId,
illust_id: illustId,
page,
user_id: imageData.userId,
url,
ext
});
}
return this.db.multiSetValues(pages);
}
async _getFiletypeCountForUser(store, userId, filetype) {
let index = store.index("user_id_and_filetype");
let query = IDBKeyRange.only([userId, 0, filetype]);
return await KeyStorage.awaitRequest(index.count(query));
}
guessFileTypeForUserId(userId) {
return this.db.dbOp(async (db) => {
let store = this.db.getStore(db);
let jpg = await this._getFiletypeCountForUser(store, userId, "jpg");
let png = await this._getFiletypeCountForUser(store, userId, "png");
if (jpg + png < 3)
return null;
let jpegFraction = jpg / (jpg + png);
if (jpegFraction > 0.9) {
console.debug(\`User \${userId} posts mostly JPEGs\`);
return "jpg";
} else if (jpegFraction < 0.1) {
console.debug(\`User \${userId} posts mostly PNGs\`);
return "png";
} else {
console.debug(
\`Not guessing file types for \${userId} due to too much variance\`
);
return null;
}
});
}
async _getStoredRecord(mediaId) {
return this.db.dbOp(async (db) => {
let store = this.db.getStore(db);
let record = await KeyStorage.asyncStoreGet(store, mediaId);
if (record == null)
return null;
else
return record.url;
});
}
async guessUrl(mediaId) {
if (ppixiv.settings.get("image_size_limit") != null)
return null;
let { type, page } = helpers.mediaId.parse(mediaId);
console.assert(type != "folder");
if (type == "file") {
let thumb2 = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (thumb2?.illustType == "video")
return null;
else
return thumb2?.mangaPages[page]?.urls?.original;
}
let mediaInfo = ppixiv.mediaCache.getMediaInfoSync(mediaId);
if (mediaInfo != null)
return mediaInfo.mangaPages[page].urls.original;
let storedUrl = await this._getStoredRecord(mediaId);
if (storedUrl != null)
return storedUrl;
let thumb = ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false });
if (thumb == null)
return null;
if (thumb.illustType == 2)
return null;
let guessedFileType = await this.guessFileTypeForUserId(thumb.userId);
if (guessedFileType == null)
return null;
let url = thumb.previewUrls[page];
url = url.replace("/c/540x540_70/", "/");
url = url.replace("/img-master/", "/img-original/");
url = url.replace("_master1200.", ".");
url = url.replace(/jpg\$/, guessedFileType);
return url;
}
async guessedUrlIncorrect(mediaId) {
await this.db.multiDelete([mediaId]);
}
};
/\x2f web/vview/util/virtual-history.js
var VirtualHistory = class {
constructor({
permanent = false
} = {}) {
this.permanent = permanent;
this.virtualUrl = null;
if (this.permanent) {
this.history = [];
this.history.push({
url: new URL(window.location),
state: window.history.state
});
window.addEventListener("popstate", (e) => {
if (e.isTrusted)
console.warn("Unexpected popstate:", e);
}, true);
}
Object.defineProperty(ppixiv, "plocation", {
get: () => {
if (this.virtualUrl != null)
return new URL(this.virtualUrl);
if (!this.permanent)
return new URL(document.location);
return new URL(this._latestHistory.url);
},
set: (value) => {
throw Error("Can't assign to ppixiv.plocation");
}
});
}
get virtual() {
return this.virtualUrl != null;
}
get _latestHistory() {
return this.history[this.history.length - 1];
}
urlIsVirtual(url) {
let args = new Args(url);
return args.hash.get("virtual");
}
get previousStateUrl() {
if (this.history.length < 2)
return null;
return this.history[this.history.length - 2].url;
}
get previousStateArgs() {
let url = this.previousStateUrl;
if (url == null)
return null;
return new Args(url);
}
get length() {
if (!this.permanent)
return window.history.length;
return this.history.length;
}
pushState(state, title, url) {
url = new URL(url, document.location);
let virtual = this.urlIsVirtual(url);
if (virtual) {
if (this.virtualUrl)
throw Error("Can't push a second virtual location");
this._virtualState = state;
this._virtualTitle = title;
this.virtualUrl = url;
return;
}
this.virtualUrl = null;
if (!this.permanent)
return window.history.pushState(state, title, url);
this.history.push({ state, url });
this._updateBrowserState();
}
replaceState(state, title, url) {
url = new URL(url, document.location);
let virtual = this.urlIsVirtual(url);
if (virtual) {
if (this.virtualUrl == null)
throw Error("Can't replace a real history entry with a virtual one");
this.virtualUrl = url;
return;
}
if (this.virtualUrl != null) {
this.virtualUrl = null;
return this.pushState(state, title, url);
}
if (!this.permanent)
return window.history.replaceState(state, title, url);
this.history.pop();
this.history.push({ state, url });
this._updateBrowserState();
}
get state() {
if (this.virtual)
return this._virtualState;
if (!this.permanent)
return window.history.state;
return this._latestHistory.state;
}
set state(value) {
if (this.virtual)
this._virtualState = value;
if (!this.permanent)
window.history.state = value;
this._latestHistory.state = value;
}
back() {
if (this.virtualUrl) {
this.virtualUrl = null;
this.broadcastPopstate({ cause: "leaving-virtual" });
return;
}
if (!this.permanent) {
window.history.back();
return;
}
if (this.history.length == 1)
return;
this.history.pop();
this.broadcastPopstate();
this._updateBrowserState();
}
broadcastPopstate({ cause } = {}) {
let e = new PopStateEvent("pp:popstate");
if (cause)
e.navigationCause = cause;
window.dispatchEvent(e);
}
_updateBrowserState() {
if (!this.permanent)
return;
try {
window.history.replaceState(this.state, "", this._latestHistory.url);
} catch (e) {
console.log("Error setting browser history (ignored)", e);
}
}
};
/\x2f web/vview/sites/site.js
var Site = class {
constructor() {
this.dataSourcesByUrl = {};
}
async init() {
return true;
}
async setInitialUrl() {
}
getDataSourceForUrl(url) {
return null;
}
createDataSourceForUrl(url, {
force = false,
startAtBeginning = false
} = {}) {
let args = new helpers.args(url);
let dataSourceClass = this.getDataSourceForUrl(url);
if (dataSourceClass == null) {
console.error("Unexpected path:", url.pathname);
return;
}
let canonicalUrl = helpers.getCanonicalUrl(url, { startAtBeginning: true }).url.toString();
let oldDataSource = this.dataSourcesByUrl[canonicalUrl];
if (!force && oldDataSource != null) {
let page = oldDataSource.getStartPage(args);
if (!oldDataSource.canLoadPage(page))
console.log(\`Not using cached data source because it can't load page \${page}\`);
else
return oldDataSource;
}
let baseUrl = helpers.getCanonicalUrl(url, { startAtBeginning }).url.toString();
let dataSource = new dataSourceClass({ url: baseUrl });
this.dataSourcesByUrl[canonicalUrl] = dataSource;
return dataSource;
}
discardDataSource(dataSource) {
let urlsToRemove = [];
for (let url in this.dataSourcesByUrl) {
if (this.dataSourcesByUrl[url] === dataSource)
urlsToRemove.push(url);
}
for (let url of urlsToRemove)
delete this.dataSourcesByUrl[url];
}
};
/\x2f web/vview/sites/native/data-sources/vview.js
var VViewBase = class extends DataSource {
get pageTitle() {
return this.getDisplayingText();
}
get isVView() {
return true;
}
get supportsStartPage() {
return true;
}
get ui() {
return UI2;
}
get autoLoadPreviousPages() {
return true;
}
constructor(args) {
super(args);
this.reachedEnd = false;
this.prevPageUuid = null;
this.nextPageUuid = null;
this.nextPageOffset = null;
this.bookmarkTagCounts = null;
}
async init() {
super.init();
this.fetchBookmarkTagCounts();
}
setStartPage(args, page) {
}
get uiInfo() {
let args = new helpers.args(this.url);
let mediaId = LocalAPI.getLocalIdFromArgs(args, { getFolder: true });
return { mediaId };
}
setPageIcon() {
helpers.setIcon({ vview: true });
}
getDisplayingText() {
let args = new helpers.args(this.url);
return LocalAPI.getSearchOptionsForArgs(args).title;
}
setUrlMediaId(mediaId, args) {
LocalAPI.getArgsForId(mediaId, args);
}
getUrlMediaId(args) {
let mediaId = LocalAPI.getLocalIdFromArgs(args);
if (mediaId == null || !mediaId.startsWith("file:"))
return null;
return mediaId;
}
get bookmarkSearchActive() {
return this.args.hash.has("bookmarks") || LocalAPI.localInfo.bookmark_tag_searches_only;
}
async fetchBookmarkTagCounts() {
if (this.fetchedBookmarkTagCounts)
return;
this.fetchedBookmarkTagCounts = true;
if (!this.bookmarkSearchActive)
return;
let result = await LocalAPI.localPostRequest(\`/api/bookmark/tags\`);
if (!result.success) {
console.log("Error fetching bookmark tag counts");
return;
}
this.bookmarkTagCounts = result.tags;
this.callUpdateListeners();
}
};
var VView = class extends VViewBase {
get name() {
return "vview";
}
constructor(url) {
super(url);
this._allIds = null;
}
async init({ targetMediaId }) {
await super.init();
if (this._initialized)
return;
this._initialized = true;
let args = new helpers.args(this.url);
let { searchOptions } = LocalAPI.getSearchOptionsForArgs(args);
let folderId = LocalAPI.getLocalIdFromArgs(args, { getFolder: true });
console.log("Loading folder contents:", folderId);
let order = args.hash.get("order");
let resultIds = await LocalAPI.localPostRequest(\`/api/ids/\${folderId}\`, {
...searchOptions,
ids_only: true,
order
});
if (!resultIds.success) {
ppixiv.message.show("Error reading directory: " + resultIds.reason);
return;
}
this.pages = PaginateMediaIds(resultIds.ids, this.estimatedItemsPerPage);
this._allIds = resultIds.ids;
this._selectInitialPage(targetMediaId);
}
getMediaIdPage(mediaId) {
if (this.pages == null)
return -1;
for (let page = 0; page < this.pages.length; ++page) {
let mediaIdsOnPage = this.pages[page];
if (mediaIdsOnPage.indexOf(mediaId) != -1)
return page;
}
return -1;
}
getStartPage(args) {
if (this.pages == null)
return 1;
let mediaId = LocalAPI.getLocalIdFromArgs(args);
if (mediaId == null)
return 1;
let page = this.getMediaIdPage(mediaId);
if (page != -1)
return page + 1;
return 1;
}
_selectInitialPage(targetMediaId) {
if (targetMediaId == null)
return;
let page = this.getMediaIdPage(targetMediaId);
if (page == -1)
return;
let newInitialPage = page + 1;
let needsReset = !this.canLoadPage(newInitialPage);
this.initialPage = newInitialPage;
console.log(\`Start on page \${this.initialPage}, reset: \${needsReset}\`);
if (needsReset)
this._resetLoadedPages();
}
getPageTextForMediaId(mediaId) {
if (this._allIds == null)
return null;
let idx = this._allIds.indexOf(mediaId);
if (idx == -1)
return null;
return \`Page \${idx + 1}/\${this._allIds.length}\`;
}
async loadPageInternal(page) {
let mediaIds = this.pages[page - 1] || [];
await ppixiv.mediaCache.batchGetMediaInfoPartial(mediaIds);
return { mediaIds };
}
};
var VViewSearch = class extends VViewBase {
get name() {
return "vview-search";
}
async loadPageInternal(page) {
if (this.reachedEnd)
return;
let lowestPage = this.idList.getLowestLoadedPage();
let highestPage = this.idList.getHighestLoadedPage();
let pageUuid = null;
let loadingDirection;
if (page == lowestPage - 1) {
pageUuid = this.prevPageUuid;
loadingDirection = "backwards";
} else if (page == highestPage + 1) {
pageUuid = this.nextPageUuid;
loadingDirection = "forwards";
} else if (this.nextPageOffset == null) {
loadingDirection = "initial";
} else {
console.error(\`Loaded unexpected page \${page} (\${lowestPage}...\${highestPage})\`);
return;
}
if (this.nextPageOffset == null) {
this.nextPageOffset = this.estimatedItemsPerPage * (page - 1);
}
let args = new helpers.args(this.url);
let { searchOptions } = LocalAPI.getSearchOptionsForArgs(args);
let folderId = LocalAPI.getLocalIdFromArgs(args, { getFolder: true });
let order = args.hash.get("order");
let result = await ppixiv.mediaCache.localSearch(folderId, {
...searchOptions,
order,
page: pageUuid,
limit: this.estimatedItemsPerPage,
skip: this.nextPageOffset
});
if (!result.success) {
ppixiv.message.show("Error reading directory: " + result.reason);
return result;
}
if (loadingDirection == "backwards" || loadingDirection == "initial")
this.prevPageUuid = result.pages.prev;
if (loadingDirection == "forwards" || loadingDirection == "initial")
this.nextPageUuid = result.pages.next;
this.nextPageOffset = result.next_offset;
if (result.pages.next == null)
this.reachedEnd = true;
let mediaIds = [];
for (let thumb of result.results)
mediaIds.push(thumb.mediaId);
return { mediaIds };
}
canLoadPage(page) {
if (page < 1)
return false;
if (this.nextPageOffset == null)
return true;
let lowestPage = this.idList.getLowestLoadedPage();
let highestPage = this.idList.getHighestLoadedPage();
return page >= lowestPage - 1 && page <= highestPage + 1;
}
};
var UI2 = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, dataSource, template: \`
<div>
<vv-container class=tag-search-box-container></vv-container>
<div class="box-button-row">
\${helpers.createBoxLink({ label: "Bookmarks", popup: "Show bookmarks", dataType: "local-bookmarks-only" })}
<div class=local-bookmark-tags-box>
\${helpers.createBoxLink({ label: "Tags", icon: "ppixiv:tag", classes: ["bookmark-tags-button"] })}
</div>
\${helpers.createBoxLink({ label: "Type", classes: ["file-type-button"] })}
\${helpers.createBoxLink({ label: "Aspect ratio", classes: ["aspect-ratio-button"] })}
\${helpers.createBoxLink({ label: "Image size", classes: ["image-size-button"] })}
\${helpers.createBoxLink({ label: "Order", classes: ["sort-button"] })}
\${helpers.createBoxLink({ label: "Reset", popup: "Clear all search options", classes: ["clear-local-search"] })}
</div>
</div>
\` });
this.dataSource = dataSource;
new LocalSearchBoxWidget({ container: this.querySelector(".tag-search-box-container") });
dataSource.setupDropdown(this.querySelector(".file-type-button"), [{
createOptions: { label: "All", dataType: "local-type-all", dataset: { default: "1" } },
setupOptions: { fields: { "#type": null } }
}, {
createOptions: { label: "Images", dataType: "local-type-images" },
setupOptions: { fields: { "#type": "images" } }
}, {
createOptions: { label: "Videos", dataType: "local-type-videos" },
setupOptions: { fields: { "#type": "videos" } }
}]);
dataSource.setupDropdown(this.querySelector(".aspect-ratio-button"), [{
createOptions: { label: "All", dataType: "local-aspect-ratio-all", dataset: { default: "1" } },
setupOptions: { fields: { "#aspect-ratio": null } }
}, {
createOptions: { label: "Landscape", dataType: "local-aspect-ratio-landscape" },
setupOptions: { fields: { "#aspect-ratio": \`3:2...\` } }
}, {
createOptions: { label: "Portrait", dataType: "local-aspect-ratio-portrait" },
setupOptions: { fields: { "#aspect-ratio": \`...2:3\` } }
}]);
dataSource.setupDropdown(this.querySelector(".image-size-button"), [{
createOptions: { label: "All", dataset: { default: "1" } },
setupOptions: { fields: { "#pixels": null } }
}, {
createOptions: { label: "High-res" },
setupOptions: { fields: { "#pixels": "4000000..." } }
}, {
createOptions: { label: "Medium-res" },
setupOptions: { fields: { "#pixels": "1000000...3999999" } }
}, {
createOptions: { label: "Low-res" },
setupOptions: { fields: { "#pixels": "...999999" } }
}]);
dataSource.setupDropdown(this.querySelector(".sort-button"), [{
createOptions: { label: "Name", dataset: { default: "1" } },
setupOptions: { fields: { "#order": null } }
}, {
createOptions: { label: "Name (inverse)" },
setupOptions: { fields: { "#order": "-normal" } }
}, {
createOptions: { label: "Newest" },
setupOptions: { fields: { "#order": "-ctime" } }
}, {
createOptions: { label: "Oldest" },
setupOptions: { fields: { "#order": "ctime" } }
}, {
createOptions: { label: "New bookmarks" },
setupOptions: {
fields: { "#order": "bookmarked-at" },
adjustUrl: (args) => args.hash.set("bookmarks", 1)
}
}, {
createOptions: { label: "Old bookmarks" },
setupOptions: {
fields: { "#order": "-bookmarked-at" },
adjustUrl: (args) => args.hash.set("bookmarks", 1)
}
}, {
createOptions: { label: "Shuffle", icon: "shuffle" },
setupOptions: { fields: { "#order": "shuffle" }, toggle: true }
}]);
class BookmarkTagDropdown extends TagDropdownWidget {
refreshTags() {
for (let tag of this.root.querySelectorAll(".following-tag"))
tag.remove();
if (this.dataSource.bookmarkTagCounts == null)
return;
this.addTagLink(null);
this.addTagLink("");
let allTags = Object.keys(this.dataSource.bookmarkTagCounts);
allTags.sort((lhs, rhs) => lhs.toLowerCase().localeCompare(rhs.toLowerCase()));
for (let tag of allTags) {
if (tag == "")
continue;
if (this.dataSource.bookmarkTagCounts[tag] == 0)
continue;
this.addTagLink(tag);
}
}
addTagLink(tag) {
let tagCount = this.dataSource.bookmarkTagCounts[tag];
let tagName = tag;
if (tagName == null)
tagName = "All bookmarks";
else if (tagName == "")
tagName = "Untagged";
let popup = null;
if (tagCount != null)
popup = tagCount + (tagCount == 1 ? " bookmark" : " bookmarks");
let a = helpers.createBoxLink({
label: tagName,
classes: ["following-tag"],
dataType: "following-tag",
popup,
link: "#",
asElement: true
});
if (tagName == "All bookmarks")
a.dataset.default = 1;
this.dataSource.setItem(a, {
fields: { "#bookmark-tag": tag }
});
this.root.appendChild(a);
}
}
this.tagDropdownOpener = new DropdownMenuOpener({
button: this.querySelector(".bookmark-tags-button"),
createDropdown: ({ ...options2 }) => new BookmarkTagDropdown({ dataSource, ...options2 })
});
this.querySelector(".local-bookmark-tags-box").hidden = !dataSource.bookmarkSearchActive;
dataSource.addEventListener("updated", () => {
this.tagDropdownOpener.setButtonPopupHighlight();
}, this._signal);
let clearLocalSearchButton = this.querySelector(".clear-local-search");
clearLocalSearchButton.addEventListener("click", (e) => {
let mediaId = LocalAPI.getLocalIdFromArgs(dataSource.args, { getFolder: true });
let args = new helpers.args("/", ppixiv.plocation);
LocalAPI.getArgsForId(mediaId, args);
helpers.navigate(args);
});
let searchActive = LocalAPI.getSearchOptionsForArgs(dataSource.args).searchOptions != null;
if (dataSource.args.hash.has("order"))
searchActive = true;
helpers.html.setClass(clearLocalSearchButton, "disabled", !searchActive);
dataSource.setItem(this.root, {
type: "local-bookmarks-only",
fields: { "#bookmarks": "1" },
toggle: true,
adjustUrl: (args) => {
if (!args.hash.has("bookmarks"))
args.hash.delete("bookmark-tag");
}
});
this.querySelector('[data-type="local-bookmarks-only"]').hidden = LocalAPI.localInfo.bookmark_tag_searches_only;
}
};
/\x2f web/vview/sites/native/data-sources/similar.js
var DataSources_VViewSimilar = class extends DataSource {
get name() {
return "similar";
}
get pageTitle() {
return this.getDisplayingText();
}
getDisplayingText() {
return \`Similar images\`;
}
get isVView() {
return true;
}
async loadPageInternal(page) {
if (page != 1)
return;
let args = new helpers.args(this.url);
let path = args.hash.get("search_path");
let url = args.hash.get("search_url");
let result = await LocalAPI.localPostRequest(\`/api/similar/search\`, {
path,
url,
max_results: 10
});
if (!result.success) {
ppixiv.message.show("Error reading search: " + result.reason);
return result;
}
this.sourceUrl = result.source_url;
this.callUpdateListeners();
let mediaIds = [];
for (let item of result.results) {
let entry = item.entry;
await ppixiv.mediaCache.addFullMediaInfo(entry);
mediaIds.push(entry.mediaId);
}
return { mediaIds };
}
canLoadPage(page) {
return page == 1;
}
setPageIcon() {
helpers.setIcon({ vview: true });
}
get uiInfo() {
let imageUrl = null;
let imageLinkUrl = null;
if (this.sourceUrl) {
imageUrl = this.sourceUrl;
let args = new helpers.args(this.url);
let path = args.hash.get("search_path");
if (path) {
let mediaId = helpers.mediaId.encodeMediaId({ type: "file", id: path });
let linkArgs = getUrlForMediaId(mediaId);
imageLinkUrl = linkArgs;
}
}
return { imageUrl, imageLinkUrl };
}
};
/\x2f web/vview/sites/native/site-native.js
var SiteNative = class extends Site {
async init() {
helpers.html.setClass(document.body, "native", ppixiv.native);
await LocalAPI.loadLocalInfo();
if (LocalAPI.localInfo.enabled && LocalAPI.localInfo.loginRequired) {
LocalAPI.redirectToLogin();
return false;
}
return true;
}
getDataSourceForUrl(url) {
url = new URL(url);
let args = new helpers.args(url);
if (args.path == "/similar")
return DataSources_VViewSimilar;
let { searchOptions } = LocalAPI.getSearchOptionsForArgs(args);
if (searchOptions == null && !LocalAPI.localInfo.bookmark_tag_searches_only)
return VView;
else
return VViewSearch;
}
async setInitialUrl() {
if (document.location.hash != "")
return;
if (LocalAPI.localInfo.bookmark_tag_searches_only) {
let args2 = helpers.args.location;
args2.hashPath = "/";
helpers.navigate(args2, { addToHistory: false, cause: "initial" });
return;
}
let mediaId = "folder:/";
let result = await ppixiv.mediaCache.localSearch(mediaId);
if (result.results.length)
mediaId = result.results[0].mediaId;
let args = helpers.args.location;
LocalAPI.getArgsForId(mediaId, args);
helpers.navigate(args, { addToHistory: false, cause: "initial" });
}
};
/\x2f web/vview/sites/pixiv/data-sources/discover-illusts.js
var DataSource_Discovery = class extends DataSource {
get name() {
return "discovery";
}
get pageTitle() {
return "Discovery";
}
getDisplayingText() {
return "Recommended Works";
}
get ui() {
return UI3;
}
get estimatedItemsPerPage() {
return 60;
}
async loadPageInternal(page) {
let mode = this.url.searchParams.get("mode") || "all";
let result = await helpers.pixivRequest.get("/ajax/discovery/artworks", {
limit: this.estimatedItemsPerPage,
mode,
lang: "en"
});
let thumbs = result.body.thumbnails.illust;
await ppixiv.mediaCache.addMediaInfosPartial(thumbs, "normal");
let mediaIds = [];
for (let thumb of thumbs)
mediaIds.push(helpers.mediaId.fromIllustId(thumb.id));
ppixiv.tagTranslations.addTranslationsDict(result.body.tagTranslation);
return { mediaIds };
}
};
var UI3 = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, template: \`
<div>
<div class=box-button-row>
\${helpers.createBoxLink({ label: "All", popup: "Show all works", dataType: "all" })}
\${helpers.createBoxLink({ label: "All ages", popup: "All ages", dataType: "safe" })}
\${helpers.createBoxLink({ label: "R18", popup: "R18", dataType: "r18", classes: ["r18"] })}
</div>
</div>
\` });
dataSource.setItem(this.root, { type: "all", fields: { mode: "all" }, defaults: { mode: "all" } });
dataSource.setItem(this.root, { type: "safe", fields: { mode: "safe" }, defaults: { mode: "all" } });
dataSource.setItem(this.root, { type: "r18", fields: { mode: "r18" }, defaults: { mode: "all" } });
}
};
/\x2f web/vview/sites/pixiv/data-sources/discover-users.js
var DataSource_DiscoverUsers = class extends DataSource {
get name() {
return "discovery_users";
}
constructor(options) {
super(options);
let args = new helpers.args(this.url);
let userId = args.hash.get("user_id");
if (userId != null)
this.showingUserId = userId;
this.seenUserIds = {};
}
get usersPerPage() {
return 20;
}
get estimatedItemsPerPage() {
let illustsPerUser = this.showingUserId != null ? 3 : 5;
return this.usersPerPage + usersPerPage * illustsPerUser;
}
async loadPageInternal(page) {
if (this.showingUserId && page > 1)
return;
if (this.showingUserId != null) {
this.userInfo = await ppixiv.userCache.getUserInfo(this.showingUserId, { full: true });
this.callUpdateListeners();
}
let result;
if (this.showingUserId != null) {
result = await helpers.pixivRequest.get(\`/ajax/user/\${this.showingUserId}/recommends\`, {
userNum: this.usersPerPage,
workNum: 8,
isR18: true,
lang: "en"
});
} else {
result = await helpers.pixivRequest.get("/ajax/discovery/users", {
limit: this.usersPerPage,
lang: "en"
});
ppixiv.tagTranslations.addTranslationsDict(result.body.tagTranslation);
}
if (result.error)
throw "Error reading suggestions: " + result.message;
await ppixiv.mediaCache.addMediaInfosPartial(result.body.thumbnails.illust, "normal");
for (let user of result.body.users) {
ppixiv.userCache.addUserData(user);
ppixiv.extraCache.addQuickUserData(user, "recommendations");
}
let recommendedUsers = result.body.recommendUsers || result.body.recommendedUsers;
let mediaIds = [];
for (let user of recommendedUsers) {
if (this.seenUserIds[user.userId])
continue;
this.seenUserIds[user.userId] = true;
mediaIds.push("user:" + user.userId);
let illustIds = user.illustIds || user.recentIllustIds;
for (let illustId of illustIds)
mediaIds.push(helpers.mediaId.fromIllustId(illustId));
}
return { mediaIds };
}
get estimatedItemsPerPage() {
return 30;
}
get pageTitle() {
if (this.showingUserId == null)
return "Recommended Users";
if (this.userInfo)
return this.userInfo.name;
else
return "Loading...";
}
getDisplayingText() {
if (this.showingUserId == null)
return "Recommended Users";
if (this.userInfo)
return "Similar artists to " + this.userInfo.name;
else
return "Illustrations";
}
};
/\x2f web/vview/sites/pixiv/data-sources/similar-illusts.js
var DataSource_SimilarIllusts = class extends DataSource {
get name() {
return "related-illusts";
}
get pageTitle() {
return "Similar Illusts";
}
getDisplayingText() {
return "Similar Illustrations";
}
get estimatedItemsPerPage() {
return 60;
}
async _loadPageAsync(page, args) {
if (!this.fetchedMediaInfo) {
this.fetchedMediaInfo = true;
let illustId = this.url.searchParams.get("illust_id");
let mediaId = helpers.mediaId.fromIllustId(illustId);
ppixiv.mediaCache.getMediaInfo(mediaId).then((mediaInfo) => {
this.mediaInfo = mediaInfo;
this.callUpdateListeners();
}).catch((e) => {
console.error(e);
});
}
return await super._loadPageAsync(page, args);
}
async loadPageInternal(page) {
if (page > 1)
return;
let mode = this.url.searchParams.get("mode") || "all";
let result = await helpers.pixivRequest.get("/ajax/discovery/artworks", {
sampleIllustId: this.url.searchParams.get("illust_id"),
mode,
limit: this.estimatedItemsPerPage,
lang: "en"
});
let thumbs = result.body.thumbnails.illust;
await ppixiv.mediaCache.addMediaInfosPartial(thumbs, "normal");
ppixiv.tagTranslations.addTranslationsDict(result.body.tagTranslation);
let mediaIds = [];
for (let thumb of thumbs)
mediaIds.push(helpers.mediaId.fromIllustId(thumb.id));
return { mediaIds };
}
get uiInfo() {
let imageUrl = null;
let imageLinkUrl = null;
if (this.mediaInfo) {
imageLinkUrl = \`/artworks/\${this.mediaInfo.illustId}#ppixiv\`;
imageUrl = this.mediaInfo.previewUrls[0];
}
return { imageUrl, imageLinkUrl };
}
};
/\x2f web/vview/sites/pixiv/data-sources/rankings.js
var DataSource_Rankings = class extends DataSource {
get name() {
return "rankings";
}
get pageTitle() {
return "Rankings";
}
getDisplayingText() {
return "Rankings";
}
get ui() {
return UI4;
}
get estimatedItemsPerPage() {
return ppixiv.mobile ? 18 : 50;
}
constructor(args) {
super(args);
this.maxPage = 999999;
}
async loadDataMobile({ date, mode, content, page }) {
let data = {
mode,
page,
type: content
};
if (date)
data.date = date;
let result = await helpers.pixivRequest.get("/touch/ajax/ranking/illust", data);
let thisDate = result.body.rankingDate;
function formatDate(date2) {
let year = date2.getUTCFullYear();
let month = date2.getUTCMonth() + 1;
let day = date2.getUTCDate();
return year + "-" + month.toString().padStart(2, "0") + "-" + day.toString().padStart(2, "0");
}
let nextDate = new Date(thisDate);
let prevDate = new Date(thisDate);
nextDate.setDate(nextDate.getDate() + 1);
prevDate.setDate(prevDate.getDate() - 1);
nextDate = formatDate(nextDate);
prevDate = formatDate(prevDate);
let lastPage = false;
let mediaIds = [];
for (let item of result.body.ranking)
mediaIds.push(helpers.mediaId.fromIllustId("" + item.illustId));
await ppixiv.mediaCache.batchGetMediaInfoPartial(mediaIds);
let foundMediaIds = [];
for (let mediaId of mediaIds) {
if (ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false }))
foundMediaIds.push(mediaId);
}
return { mediaIds: foundMediaIds, thisDate, nextDate, prevDate, lastPage };
}
async loadDataDesktop({ date, mode, content, page }) {
let data = {
content,
mode,
format: "json",
p: page
};
if (date)
data.date = date;
let result = await helpers.pixivRequest.get("/ranking.php", data);
let thisDate = result.date;
let nextDate = result.next_date;
let prevDate = result.prev_date;
let lastPage = !result.next;
if (!nextDate)
nextDate = null;
if (!prevDate)
prevDate = null;
if (thisDate.length == 8) {
let year = thisDate.slice(0, 4);
let month = thisDate.slice(4, 6);
let day = thisDate.slice(6, 8);
thisDate = year + "/" + month + "/" + day;
}
let isAI = mode == "daily_ai" || mode == "daily_r18_ai";
for (let illust of result.contents)
illust.aiType = isAI ? 2 : 1;
let mediaIds = [];
for (let item of result.contents)
mediaIds.push(helpers.mediaId.fromIllustId("" + item.illust_id));
await ppixiv.mediaCache.addMediaInfosPartial(result.contents, "rankings");
return { mediaIds, thisDate, nextDate, prevDate, lastPage };
}
loadDataForPlatform(options) {
if (ppixiv.mobile)
return this.loadDataMobile(options);
else
return this.loadDataDesktop(options);
}
async loadPageInternal(page) {
if (page > this.maxPage)
return;
let queryArgs = this.url.searchParams;
let date = queryArgs.get("date");
let mode = queryArgs.get("mode") ?? "daily";
let content = queryArgs.get("content") ?? "all";
let { mediaIds, thisDate, nextDate, prevDate, lastPage } = await this.loadDataForPlatform({ date, mode, content, page });
if (lastPage)
this.maxPage = Math.min(page, this.maxPage);
this.todayText ??= thisDate;
this.prevDate = prevDate;
this.nextDate = nextDate;
this.callUpdateListeners();
return { mediaIds };
}
};
var UI4 = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, template: \`
<div class="ranking-data-source box-button-row">
<div class="box-button-row date-row">
\${helpers.createBoxLink({ label: "Previous day", popup: "Show the previous day", dataType: "new-illust-type-illust", classes: ["nav-yesterday"] })}
<span class=nav-today></span>
\${helpers.createBoxLink({ label: "Next day", popup: "Show the next day", dataType: "new-illust-type-illust", classes: ["nav-tomorrow"] })}
</div>
<div class=box-button-row>
\${helpers.createBoxLink({ label: "Ranking type", popup: "Rankings to display", classes: ["mode-button"] })}
\${helpers.createBoxLink({ label: "Contents", popup: "Content type to display", classes: ["content-type-button"] })}
</div>
<div class="box-button-row modes"></div>
</div>
\` });
this.dataSource = dataSource;
dataSource.addEventListener("updated", () => this.refreshDates(), this._signal);
this.refreshDates();
let rankingTypes = {
"daily": { content: ["o", "o*", "i", "i*", "u", "u*"], label: "Daily", popup: "Daily rankings" },
"weekly": { content: ["o", "o*", "i", "i*", "u", "u*", "o**", "i**"], label: "Weekly", popup: "Weekly rankings" },
"monthly": { content: ["o", "i"], label: "Monthly", popup: "Monthly rankings" },
"rookie": { content: ["o", "i"], label: "Rookie", popup: "Rookie rankings" },
"original": { content: ["o"], label: "Original", popup: "Original rankings" },
"daily_ai": { content: ["o", "o*"], label: "AI", popup: "Show AI works" },
"male": { content: ["o", "o*"], label: "Male", popup: "Popular with men" },
"female": { content: ["o", "o*"], label: "Female", popup: "Popular with women" }
};
function contentKeyFor(content, ages) {
let keys = { "all": "o", "illust": "i", "manga": "i", "ugoira": "u" };
let contentKey2 = keys[content];
if (ages == "r18")
contentKey2 += "*";
else if (ages == "r18g")
contentKey2 += "**";
return contentKey2;
}
function modeWithAges(mode, ages) {
if (ages == "r18")
mode += "_r18";
else if (ages == "r18g")
mode += "_r18g";
if (mode == "daily_ai_r18")
mode = "daily_r18_ai";
else if (mode == "weekly_r18g")
mode = "r18g";
return mode;
}
let currentArgs = new helpers.args(dataSource.url);
let currentContent = currentArgs.query.get("content") || "all";
let currentMode = currentArgs.query.get("mode") || "daily";
if (currentMode == "r18g")
currentMode = "weekly_r18g";
let currentAges = currentMode.indexOf("r18g") != -1 ? "r18g" : currentMode.indexOf("r18") != -1 ? "r18" : "all";
currentMode = currentMode.replace("_r18g", "").replace("_r18", "");
let contentKey = contentKeyFor(currentContent, currentAges);
console.log(\`Rankings content mode: \${currentContent}, ages: \${currentAges}, key: \${contentKey}\`);
let modeContainer = this.querySelector(".modes");
for (let agesToggle of ["r18", "r18g"]) {
let targetMode = currentMode;
let currentRankingType = rankingTypes[currentMode];
console.assert(currentRankingType, currentMode);
let { content } = currentRankingType;
let button = helpers.createBoxLink({
label: agesToggle.toUpperCase(),
popup: \`Show \${agesToggle.toUpperCase()} works\`,
classes: [agesToggle],
asElement: true
});
modeContainer.appendChild(button);
let contentKeyForMode = contentKeyFor(currentContent, agesToggle);
if (content.indexOf(contentKeyForMode) == -1)
targetMode = agesToggle == "r18" ? "daily" : "weekly";
let modeEnabled = modeWithAges(targetMode, agesToggle);
let modeDisabled = modeWithAges(targetMode, "all");
dataSource.setItem(button, {
fields: { mode: modeEnabled },
toggle: true,
classes: [agesToggle],
adjustUrl: (args) => {
if (currentAges == agesToggle)
args.query.set("mode", modeDisabled);
}
});
}
new DropdownMenuOpener({
button: this.querySelector(".content-type-button"),
createDropdown: ({ ...options2 }) => {
let dropdown = new Widget({
...options2,
template: \`
<div class="vertical-list">
\${helpers.createBoxLink({ label: "All", popup: "Show all works", dataType: "content-all" })}
\${helpers.createBoxLink({ label: "Illustrations", popup: "Show illustrations only", dataType: "content-illust" })}
\${helpers.createBoxLink({ label: "Animations", popup: "Show animations only", dataType: "content-ugoira" })}
\${helpers.createBoxLink({ label: "Manga", popup: "Show manga only", dataType: "content-manga" })}
</div>
\`
});
for (let content of ["all", "illust", "ugoira", "manga"]) {
dataSource.setItem(dropdown, {
type: "content-" + content,
fields: { content },
defaults: { content: "all" },
adjustUrl: (args) => {
if (content == currentContent)
return;
let currentRankingType = rankingTypes[currentMode];
console.assert(currentRankingType, currentMode);
let switching_to_content_key = contentKeyFor(content, currentAges);
if (currentRankingType.content.indexOf(switching_to_content_key) == -1)
args.query.set("mode", "daily");
}
});
}
return dropdown;
}
});
new DropdownMenuOpener({
button: this.querySelector(".mode-button"),
createDropdown: ({ ...options2 }) => {
let dropdown = new Widget({
...options2,
template: \`
<div class="vertical-list">
</div>
\`
});
for (let [mode, { content, label, popup }] of Object.entries(rankingTypes)) {
console.assert(content, mode);
mode = modeWithAges(mode, currentAges);
if (content.indexOf(contentKey) == -1)
continue;
let button = helpers.createBoxLink({
label,
popup,
asElement: true
});
dropdown.root.appendChild(button);
dataSource.setItem(button, {
fields: { mode },
defaults: { mode: "daily" }
});
}
return dropdown;
}
});
}
refreshDates = () => {
if (this.dataSource.todayText)
this.querySelector(".nav-today").innerText = this.dataSource.todayText;
let yesterday = this.querySelector(".nav-yesterday");
helpers.html.setClass(yesterday, "disabled", this.dataSource.prevDate == null);
if (this.dataSource.prevDate) {
let url = new URL(this.dataSource.url);
url.searchParams.set("date", this.dataSource.prevDate);
yesterday.href = url;
}
let tomorrow = this.querySelector(".nav-tomorrow");
helpers.html.setClass(tomorrow, "disabled", this.dataSource.nextDate == null);
if (this.dataSource.nextDate) {
let url = new URL(this.dataSource.url);
url.searchParams.set("date", this.dataSource.nextDate);
tomorrow.href = url;
}
};
};
/\x2f web/vview/sites/pixiv/data-sources/artist.js
var DataSources_Artist = class extends DataSource {
get name() {
return "artist";
}
get ui() {
return UI5;
}
constructor({ url, ...args }) {
url = new URL(url);
let parts = url.pathname.split("/");
if (parts.length == 3) {
parts.push("artworks");
url.pathname = parts.join("/");
}
url = url.toString();
super({ url, ...args });
}
get supportsStartPage() {
return true;
}
get viewingUserId() {
return helpers.strings.getPathPart(this.url, 1);
}
get viewingType() {
let url = helpers.pixiv.getUrlWithoutLanguage(this.url);
let parts = url.pathname.split("/");
return parts[3] || "artworks";
}
async loadPageInternal(page) {
let currentTag = this.currentTag;
if (currentTag != null) {
this.translatedTags = await ppixiv.tagTranslations.getTranslations(
[currentTag],
"en"
);
this.callUpdateListeners();
}
let userInfoPromise = this._loadUserInfo();
let args = new helpers.args(this.url);
let tag = this.currentTag;
if (tag == null) {
if (this.pages == null) {
let allMediaIds = await this.loadAllResults();
if (args.hash.get("order") == "oldest")
allMediaIds.reverse();
this.pages = PaginateMediaIds(allMediaIds, this.estimatedItemsPerPage);
}
let mediaIds = this.pages[page - 1] || [];
await ppixiv.mediaCache.batchGetMediaInfoPartial(mediaIds, {
userId: this.viewingUserId
});
return { mediaIds };
} else {
let type = args.query.get("type");
let typeForUrl = type == null ? "illustmanga" : type == "illust" ? "illusts" : "manga";
let requestUrl = \`/ajax/user/\${this.viewingUserId}/\${typeForUrl}/tag\`;
let result = await helpers.pixivRequest.get(requestUrl, {
tag,
offset: (page - 1) * 48,
limit: 48
});
await userInfoPromise;
for (let item of result.body.works) {
item.userName = this.userInfo.name;
item.profileImageUrl = this.userInfo.imageBig;
}
let mediaIds = [];
for (let illustData of result.body.works)
mediaIds.push(helpers.mediaId.fromIllustId(illustData.id));
await ppixiv.mediaCache.addMediaInfosPartial(result.body.works, "normal");
return { mediaIds };
}
}
async _loadUserInfo() {
this.userInfo = await ppixiv.userCache.getUserInfo(this.viewingUserId, {
full: true
});
this.callUpdateListeners();
}
async loadAllResults() {
let type = this.viewingType;
let result = await helpers.pixivRequest.get(
\`/ajax/user/\${this.viewingUserId}/profile/all\`
);
let illustIds = [];
if (type == "artworks" || type == "illustrations")
for (let illustId in result.body.illusts)
illustIds.push(illustId);
if (type == "artworks" || type == "manga")
for (let illustId in result.body.manga)
illustIds.push(illustId);
illustIds.sort((lhs, rhs) => parseInt(rhs) - parseInt(lhs));
let mediaIds = [];
for (let illustId of illustIds)
mediaIds.push(helpers.mediaId.fromIllustId(illustId));
return mediaIds;
}
get currentTag() {
let args = new helpers.args(helpers.pixiv.getUrlWithoutLanguage(this.url));
let tag = args.get("/3");
if (tag == null)
tag = args.query.get("tag");
return tag;
}
get uiInfo() {
let headerStripURL = this.userInfo?.background?.url;
if (headerStripURL) {
headerStripURL = new URL(headerStripURL);
helpers.pixiv.adjustImageUrlHostname(headerStripURL);
}
return {
mediaId: \`user:\${this.viewingUserId}\`,
userId: this.viewingUserId,
mobileTitle: this.userInfo?.name ? \`Artist: \${this.userInfo?.name}\` : \`Artist\`,
headerStripURL
};
}
async tagListOpened() {
let userInfo = await ppixiv.userCache.getUserInfo(this.viewingUserId, {
full: true
});
console.log("Loading tags for user", userInfo.userId);
this.postTags = await this.getUserTags(userInfo);
let userTagSearch = SavedSearchTags.getAllUsedTags();
for (let tag of this.postTags)
tag.recent = userTagSearch.has(tag.tag);
this.postTags.sort((lhs, rhs) => {
if (rhs.recent != lhs.recent)
return rhs.recent - lhs.recent;
else
return rhs.cnt - lhs.cnt;
});
let tags = [];
for (let tagInfo of this.postTags)
tags.push(tagInfo.tag);
this.translatedTags = await ppixiv.tagTranslations.getTranslations(
tags,
"en"
);
this.callUpdateListeners();
}
async getUserTags(userInfo) {
if (userInfo.frequentTags)
return Array.from(userInfo.frequentTags);
let result = await helpers.pixivRequest.get(
"/ajax/user/" + userInfo.userId + "/illustmanga/tags",
{}
);
if (result.error) {
console.error(
"Error fetching tags for user " + userInfo.userId + ": " + result.error
);
userInfo.frequentTags = [];
return Array.from(userInfo.frequentTags);
}
result.body.sort(function(lhs, rhs) {
return rhs.cnt - lhs.cnt;
});
let translations = [];
for (let tagInfo of result.body) {
if (tagInfo.tag_translation == "")
continue;
translations.push({
tag: tagInfo.tag,
translation: {
en: tagInfo.tag_translation
}
});
}
ppixiv.tagTranslations.addTranslations(translations);
userInfo.frequentTags = result.body;
return Array.from(userInfo.frequentTags);
}
get pageTitle() {
if (this.userInfo)
return this.userInfo.name;
else
return "Loading...";
}
getDisplayingText() {
if (this.userInfo)
return this.userInfo.name + "'s Illustrations";
else
return "Illustrations";
}
};
var UI5 = class extends Widget {
constructor({ dataSource, ...options }) {
super({
...options,
template: \`
<div>
<div class="box-button-row" style="align-items: flex-start">
\${helpers.createBoxLink({ label: "Search mode", classes: ["search-type-button"] })}
\${helpers.createBoxLink({ label: "Newest", classes: ["sort-button"] })}
\${helpers.createBoxLink({ label: "Tags", popup: "Tags", icon: "bookmark", classes: ["member-tags-button"] })}
</div>
</div>
\`
});
this.dataSource = dataSource;
dataSource.addEventListener(
"updated",
() => {
this.tagDropdown.setButtonPopupHighlight();
},
this._signal
);
const urlFormat = "users/id/type/tag";
dataSource.setupDropdown(this.querySelector(".search-type-button"), [
{
createOptions: { label: "Works" },
setupOptions: { urlFormat, fields: { "/type": "artworks" } }
},
{
createOptions: { label: "Illusts" },
setupOptions: { urlFormat, fields: { "/type": "illustrations" } }
},
{
createOptions: { label: "Manga" },
setupOptions: { urlFormat, fields: { "/type": "manga" } }
}
]);
let sortButton = this.querySelector(".sort-button");
let tag = dataSource.currentTag;
sortButton.hidden = tag != null;
dataSource.setupDropdown(sortButton, [
{
createOptions: { label: "Newest", dataset: { default: true } },
setupOptions: {
fields: { "#order": null },
defaults: { "#order": "newest" }
}
},
{
createOptions: { label: "Oldest" },
setupOptions: { fields: { "#order": "oldest" } }
}
]);
class TagDropdown extends TagDropdownWidget {
refreshTags() {
helpers.html.removeElements(this.root);
if (dataSource.postTags != null) {
this.addTagLink({ tag: "All" });
for (let tagInfo of dataSource.postTags || [])
this.addTagLink(tagInfo);
} else {
let span = document.createElement("span");
span.innerText = "Loading...";
this.root.appendChild(span);
this.addTagLink({ tag: "All" });
let currentTag = dataSource.currentTag;
if (currentTag != null)
this.addTagLink({ tag: currentTag });
}
}
addTagLink(tagInfo) {
if (tagInfo.tag != "All" && tagInfo.cnt < 5 && !tagInfo.recent)
return;
let tag2 = tagInfo.tag;
let translatedTag = tag2;
if (dataSource.translatedTags && dataSource.translatedTags[tag2])
translatedTag = dataSource.translatedTags[tag2];
let classes = ["tag-entry"];
if (tagInfo.recent)
classes.push("recent");
let a = helpers.createBoxLink({
label: translatedTag,
classes,
popup: tagInfo?.cnt,
link: "#",
asElement: true,
dataType: "artist-tag"
});
dataSource.setItem(a, {
urlFormat,
fields: { "/tag": tag2 != "All" ? tag2 : null }
});
if (tag2 == "All")
a.dataset["default"] = 1;
this.root.appendChild(a);
}
}
this.tagDropdown = new DropdownMenuOpener({
button: this.querySelector(".member-tags-button"),
createDropdown: ({ ...options2 }) => new TagDropdown({ dataSource, ...options2 }),
onVisibilityChanged: (opener) => {
if (opener.visible)
;
dataSource.tagListOpened();
}
});
}
};
/\x2f web/vview/sites/pixiv/data-sources/illust.js
var DataSource_Illust = class extends DataSource {
get name() {
return "illust";
}
constructor(args) {
super(args);
this.mediaId = this.getUrlMediaId(new helpers.args(this.url));
this._loadMediaInfo();
}
async _loadMediaInfo() {
this.mediaInfo = await ppixiv.mediaCache.getMediaInfo(this.mediaId, { full: false });
}
get defaultScreen() {
return "illust";
}
async loadPageInternal(page) {
}
getUrlMediaId(args) {
let url = args.url;
url = helpers.pixiv.getUrlWithoutLanguage(url);
let parts = url.pathname.split("/");
let illustId = parts[2];
let page = this.getUrlMangaPage(args);
return helpers.mediaId.fromIllustId(illustId, page);
}
get searchUrl() {
if (this.mediaInfo)
return new URL(\`/users/\${this.mediaInfo.userId}/artworks#ppixiv\`, this.url);
else
return this.url;
}
setUrlMediaId(mediaId, args) {
let [illustId, page] = helpers.mediaId.toIllustIdAndPage(mediaId);
let parts = args.path.split("/");
let id_part = parts[1].length == 2 ? 3 : 2;
parts[id_part] = illustId;
args.path = parts.join("/");
args.hash.set("page", page + 1);
}
};
/\x2f web/vview/sites/pixiv/data-sources/followed-users.js
var DataSource_Follows = class extends DataSource {
get name() {
return "following";
}
get supportsStartPage() {
return true;
}
get ui() {
return UI6;
}
constructor(args) {
super(args);
this.followTags = [];
}
get viewingUserId() {
if (helpers.strings.getPathPart(this.url, 0) == "users") {
return helpers.strings.getPathPart(this.url, 1);
}
let queryArgs = this.url.searchParams;
let userId = queryArgs.get("id");
if (userId == null)
return ppixiv.pixivInfo.userId;
return userId;
}
async loadPageInternal(page) {
this.userInfo = await ppixiv.userCache.getUserInfo(this.viewingUserId, { full: true });
this.callUpdateListeners();
let queryArgs = this.url.searchParams;
let rest = queryArgs.get("rest") || "show";
let acceptingRequests = queryArgs.get("acceptingRequests") || "0";
let url = "/ajax/user/" + this.viewingUserId + "/following";
let args = {
offset: this.estimatedItemsPerPage * (page - 1),
limit: this.estimatedItemsPerPage,
rest,
acceptingRequests
};
if (queryArgs.get("tag"))
args.tag = queryArgs.get("tag");
let result = await helpers.pixivRequest.get(url, args);
this.followTags = result.body.followUserTags;
this.followTags.sort((lhs, rhs) => lhs.toLowerCase().localeCompare(rhs.toLowerCase()));
this.callUpdateListeners();
let illusts = [];
for (let followedUser of result.body.users) {
if (followedUser == null)
continue;
ppixiv.extraCache.addQuickUserData(followedUser, "following");
if (!followedUser.illusts.length) {
console.log("Can't show followed user that has no posts:", followedUser.userId);
continue;
}
let illust = followedUser.illusts[0];
illusts.push(illust);
illust.profileImageUrl = followedUser.profileImageUrl;
}
let mediaIds = [];
for (let illust of illusts)
mediaIds.push("user:" + illust.userId);
await ppixiv.mediaCache.addMediaInfosPartial(illusts, "normal");
return { mediaIds };
}
get uiInfo() {
return {
userId: this.viewingSelf ? null : this.viewingUserId
};
}
get viewingSelf() {
return this.viewingUserId == ppixiv.pixivInfo.userId;
}
get pageTitle() {
if (!this.viewingSelf) {
if (this.userInfo)
return this.userInfo.name + "'s Follows";
return "User's follows";
}
let queryArgs = this.url.searchParams;
let privateFollows = queryArgs.get("rest") == "hide";
return privateFollows ? "Private follows" : "Followed users";
}
getDisplayingText() {
if (!this.viewingSelf) {
if (this.userInfo)
return this.userInfo.name + "'s followed users";
return "User's followed users";
}
let queryArgs = this.url.searchParams;
let privateFollows = queryArgs.get("rest") == "hide";
return privateFollows ? "Private follows" : "Followed users";
}
};
var UI6 = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, template: \`
<div>
<div class=box-button-row>
<div class=box-button-row>
<vv-container class=follows-public-private style="margin-right: 25px;">
\${helpers.createBoxLink({ label: "Public", popup: "Show publically followed users", dataType: "public-follows" })}
\${helpers.createBoxLink({ label: "Private", popup: "Show privately followed users", dataType: "private-follows" })}
</vv-container>
\${helpers.createBoxLink({ popup: "Accepting requests", icon: "paid", dataType: "accepting-requests" })}
</div>
\${helpers.createBoxLink({ label: "All tags", popup: "Follow tags", icon: "bookmark", classes: ["follow-tags-button", "premium-only"] })}
</div>
</div>
\` });
this.dataSource = dataSource;
let publicPrivateButtonContainer = this.querySelector(".follows-public-private");
publicPrivateButtonContainer.hidden = !dataSource.viewingSelf;
dataSource.setItem(this.root, { type: "public-follows", fields: { rest: "show" }, defaults: { rest: "show" } });
dataSource.setItem(this.root, { type: "private-follows", fields: { rest: "hide" }, defaults: { rest: "show" } });
dataSource.setItem(this.root, { type: "accepting-requests", toggle: true, fields: { acceptingRequests: "1" }, defaults: { acceptingRequests: "0" } });
class FollowTabDropdown extends Widget {
constructor() {
super({
...options,
template: \`<div class="follow-tag-list vertical-list"></div>\`
});
dataSource.addEventListener("updated", () => this.refreshFollowingTags(), this._signal);
this.refreshFollowingTags();
}
refreshFollowingTags() {
let tagList = this.root;
for (let tag of tagList.querySelectorAll(".tag-entry"))
tag.remove();
let currentTag = dataSource.url.searchParams.get("tag") || "All tags";
let addTagLink = (tag) => {
if (tag == "null")
return;
let a = helpers.createBoxLink({
label: tag,
classes: ["tag-entry"],
link: "#",
asElement: true,
dataType: "following-tag"
});
if (tag == "All tags") {
tag = null;
a.dataset.default = 1;
}
dataSource.setItem(a, { fields: { "tag": tag } });
tagList.appendChild(a);
};
addTagLink("All tags");
for (let tag of dataSource.followTags)
addTagLink(tag);
if (dataSource.followTags.length == 0 && currentTag != "All tags")
addTagLink(currentTag);
}
}
new DropdownMenuOpener({
button: this.querySelector(".follow-tags-button"),
createDropdown: ({ ...options2 }) => new FollowTabDropdown({ dataSource, ...options2 })
});
}
};
/\x2f web/vview/sites/pixiv/data-sources/manga-pages.js
var DataSource_MangaPages = class extends DataSource {
get name() {
return "manga";
}
get allowExpandingMangaPages() {
return false;
}
constructor(args) {
super(args);
let url = new URL(this.url);
url = helpers.pixiv.getUrlWithoutLanguage(url);
let parts = url.pathname.split("/");
let illustId = parts[2];
this.mediaId = helpers.mediaId.fromIllustId(illustId);
}
async loadPageInternal(page) {
if (page != 1)
return;
this.mediaInfo = await ppixiv.mediaCache.getMediaInfo(this.mediaId, { full: false });
if (this.mediaInfo == null)
return;
this.callUpdateListeners();
let mediaIds = [];
for (let page2 = 0; page2 < this.mediaInfo.pageCount; ++page2)
mediaIds.push(helpers.mediaId.getMediaIdForPage(this.mediaId, page2));
let { promise } = ppixiv.extraCache.batchGetMediaAspectRatio(mediaIds);
await promise;
return { mediaIds };
}
get pageTitle() {
if (this.mediaInfo)
return this.mediaInfo.userName + " - " + this.mediaInfo.illustTitle;
else
return "Illustrations";
}
getDisplayingText() {
if (this.mediaInfo)
return this.mediaInfo.illustTitle + " by " + this.mediaInfo.userName;
else
return "Illustrations";
}
get uiInfo() {
return {
userId: this.mediaInfo?.userId
};
}
};
/\x2f web/vview/sites/pixiv/data-sources/series.js
var DataSource_MangaPages2 = class extends DataSource {
get name() {
return "series";
}
constructor(args) {
super(args);
this.seriesInfo = null;
this.userInfo = null;
let url = new URL(this.url);
url = helpers.pixiv.getUrlWithoutLanguage(url);
let parts = url.pathname.split("/");
this.seriesId = parts[4];
}
async loadPageInternal(page) {
let url = \`/ajax/series/\${this.seriesId}\`;
let result = await helpers.pixivRequest.get(url, { p: page });
if (result.error) {
ppixiv.message.show("Error reading series: " + result.message);
return;
}
let { body } = result;
let translations = [];
for (let tag of Object.keys(body.tagTranslation)) {
translations.push({
tag,
translation: body.tagTranslation[tag]
});
}
ppixiv.tagTranslations.addTranslations(translations);
this.seriesInfo = helpers.other.findById(body.illustSeries, "id", this.seriesId);
this.userInfo = helpers.other.findById(body.users, "userId", this.seriesInfo.userId);
this.callUpdateListeners();
await ppixiv.mediaCache.addMediaInfosPartial(body.thumbnails.illust, "normal");
let mediaIds = [];
let seriesPageInfo = body.page;
let seriesPages = seriesPageInfo.series;
seriesPages.sort((lhs, rhs) => rhs.order - lhs.order);
for (let seriesPage of seriesPages) {
let illustId = seriesPage.workId;
let mediaId = helpers.mediaId.fromIllustId(illustId, 0);
mediaIds.push(mediaId);
}
return { mediaIds };
}
get pageTitle() {
if (this.seriesInfo)
return this.userInfo.name + " - " + this.seriesInfo.title;
else
return "Series";
}
getDisplayingText() {
if (this.seriesInfo)
return this.userInfo.name + " - " + this.seriesInfo.title;
else
return "Series";
}
get uiInfo() {
let headerStripURL = this.seriesInfo?.url;
return {
userId: this.userInfo?.userId,
headerStripURL
};
}
};
/\x2f web/vview/widgets/tag-search-dropdown.js
var TagSearchBoxWidget = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, template: \`
<div class="search-box tag-search-box">
<div class="input-field-container hover-menu-box">
<input placeholder=Tags size=1 autocorrect=off>
<span class="edit-search-button right-side-button">
\${helpers.createIcon("mat:edit")}
</span>
<span class="related-tags-button right-side-button">
\${helpers.createIcon("bookmark")}
</span>
<span class="search-submit-button right-side-button">
\${helpers.createIcon("search")}
</span>
</div>
</div>
\` });
this.dataSource = dataSource;
this.dataSource.addEventListener("updated", () => this.refresh(), this._signal);
this._inputElement = this.root.querySelector(".input-field-container > input");
this.querySelector(".edit-search-button").addEventListener("click", (e) => {
this._dropdownOpener.visible = true;
this._dropdownOpener.dropdown.editing = !this._dropdownOpener.dropdown.editing;
});
this._dropdownOpener = new DropdownBoxOpener({
button: this._inputElement,
createDropdown: ({ ...options2 }) => {
let dropdown = new TagSearchDropdownWidget({
inputElement: this.root,
parent: this,
savedPosition: this._savedDropdownPosition,
textPrompt: (args) => this.textPrompt(args),
...options2
});
dropdown.shutdownSignal.addEventListener("abort", () => {
this._savedDropdownPosition = dropdown._saveSearchPosition();
});
return dropdown;
},
shouldCloseForClick: (e) => {
if (this._showingDialog)
return false;
if (helpers.html.isAbove(this.root, e.target))
return false;
return true;
}
});
this.tagDropdown = new DropdownMenuOpener({
button: this.querySelector(".related-tags-button"),
createDropdown: ({ ...options2 }) => new RelatedTagDropdown({ dataSource, ...options2 })
});
this._inputElement.addEventListener("focus", () => this._dropdownOpener.visible = true, true);
helpers.inputHandler(this._inputElement, this._submitSearch);
this.root.querySelector(".search-submit-button").addEventListener("click", this._submitSearch);
}
visibilityChanged() {
super.visibilityChanged();
if (!this.visibleRecursively)
this._dropdownOpener.visible = false;
}
refresh() {
super.refresh();
helpers.html.setClass(this.querySelector(".related-tags-button"), "disabled", this.dataSource?.relatedTags == null);
}
async dialog(promise) {
this._showingDialog = true;
try {
return await promise;
} finally {
this._inputElement.focus();
this._showingDialog = false;
}
}
textPrompt(options) {
return this.dialog(TextPrompt.prompt(options));
}
confirmPrompt(options) {
return this.dialog(ConfirmPrompt.prompt(options));
}
_submitSearch = (e) => {
let tags = this._inputElement.value.trim();
if (tags.length == 0)
return;
SavedSearchTags.add(tags);
if (e.target instanceof HTMLInputElement) {
e.target.blur();
this._dropdownOpener.visible = false;
}
let args = helpers.getArgsForTagSearch(tags, ppixiv.plocation);
helpers.navigate(args);
};
};
var TagSearchDropdownWidget = class extends Widget {
constructor({ inputElement, savedPosition, textPrompt, ...options }) {
super({ ...options, template: \`
<div class="search-history input-dropdown" tabindex=1>
<div class=input-dropdown-list>
<div class="tag-section create-section-button editing-only">
<div class="edit-button">
\${helpers.createIcon("mat:create_new_folder")}
</div>
<div class=label>Add section</div>
</div>
<!-- template-tag-dropdown-entry instances will be added here. -->
<vv-container class=contents></vv-container>
</div>
</div>
\` });
this._autocompleteCache = /* @__PURE__ */ new Map();
this._disableAutocompleteUntil = 0;
this.savedPosition = savedPosition;
this.textPrompt = textPrompt;
this._inputElement = inputElement.querySelector("input");
this._inputElement.addEventListener("keydown", this._inputKeydown);
this._inputElement.addEventListener("input", this.inputOnInput);
document.addEventListener("selectionchange", this._inputSelectionChange, { signal: this.shutdownSignal });
window.addEventListener("recent-tag-searches-changed", this._populateDropdown, { signal: this.shutdownSignal });
window.addEventListener("pp:popstate", this._selectCurrentSearch, { signal: this.shutdownSignal });
this.root.addEventListener("click", this._dropdownClick);
this._currentAutocompleteResults = {};
this._allResults = this.root;
this._inputDropdown = this.root.querySelector(".input-dropdown-list");
this._inputDropdownContents = this._inputDropdown.querySelector(".contents");
let observer = new MutationObserver((mutations) => {
let width2 = parseInt(this.root.style.width);
if (isNaN(width2))
width2 = 600;
ppixiv.settings.set("tag-dropdown-width", width2);
});
observer.observe(this.root, { attributes: true });
this._inputDropdown.style.width = ppixiv.settings.get("tag-dropdown-width", "400px");
let width = ppixiv.settings.get("tag-dropdown-width", "400");
width = parseInt(width);
this.root.style.setProperty("--width", \`\${width}px\`);
this.dragger = new DragHandler({
parent: this,
name: "search-dragger",
element: this.root,
confirmDrag: ({ event }) => event.target.closest(".drag-handle") != null,
ondragstart: (args) => this._ondragstart(args),
ondrag: (args) => this._ondrag(args),
ondragend: (args) => this._ondragend(args)
});
this.editing = false;
this._load();
}
get editing() {
return this._editing;
}
set editing(value) {
if (this._editing == value)
return;
this._editing = value;
helpers.html.setClass(this.root, "editing", this._editing);
helpers.html.setClass(this.root.querySelector(".input-dropdown-list"), "editing", this._editing);
}
_findTagEntry(tag) {
for (let entry of this._inputDropdown.querySelectorAll(".entry[data-tag]")) {
if (entry.dataset.tag == tag)
return entry;
}
return null;
}
_ondragstart({ event }) {
let dragHandle = event.target.closest(".drag-handle");
let entry = dragHandle.closest(".entry");
this.draggingTag = entry.dataset.tag;
return true;
}
_ondrag({ event }) {
function findSibling(entry2, next) {
let sibling = entry2;
while (sibling) {
if (next)
sibling = sibling.nextElementSibling;
else
sibling = sibling.previousElementSibling;
if (sibling == null)
return null;
if (!sibling.classList.contains("collapsed"))
return sibling;
}
return null;
}
let entry = this._findTagEntry(this.draggingTag);
let entryRect = entry.getBoundingClientRect();
for (let down = 0; down <= 1; down++) {
let entryToCheck = findSibling(entry, down == 1);
if (entryToCheck == null)
continue;
if (!entryToCheck.classList.contains("saved") && !entryToCheck.classList.contains("tag-section"))
continue;
let neighborRect = entryToCheck.getBoundingClientRect();
let threshold = 5;
if (down) {
let y = (neighborRect.bottom + entryRect.top) / 2;
if (event.clientY - threshold < y)
continue;
} else {
let y = (entryRect.bottom + neighborRect.top) / 2;
if (event.clientY + threshold > y)
continue;
}
let entryToPlaceAfter = entryToCheck;
if (!down)
entryToPlaceAfter = findSibling(entryToCheck, false);
if (entryToPlaceAfter == null)
continue;
let moveAfterIdx = -1;
if (entryToPlaceAfter.groupName)
moveAfterIdx = SavedSearchTags.findIndex({ group: entryToPlaceAfter.groupName });
else if (entryToPlaceAfter.dataset.tag)
moveAfterIdx = SavedSearchTags.findIndex({ tag: entryToPlaceAfter.dataset.tag });
if (moveAfterIdx != -1) {
SavedSearchTags.move(this.draggingTag, moveAfterIdx + 1);
return;
}
}
}
_ondragend({ event }) {
this.draggingTag = null;
}
_getSectionHeaderForGroup(group) {
for (let tagSection of this.root.querySelectorAll(".tag-section")) {
if (tagSection.groupName == group)
return tagSection;
}
return null;
}
getEntryForTag(tag, { includeAutocomplete = false } = {}) {
tag = tag.trim();
for (let entry of this.root.querySelectorAll(".entry")) {
if (!includeAutocomplete && entry.classList.contains("autocomplete"))
continue;
if (entry.dataset.tag.trim() == tag)
return entry;
}
return null;
}
_dropdownClick = async (e) => {
let entry = e.target.closest(".entry");
let tagSection = e.target.closest(".tag-section");
let createSectionButton = e.target.closest(".create-section-button");
if (createSectionButton) {
e.stopPropagation();
e.preventDefault();
let label = await this.textPrompt({ title: "Group name:" });
if (label == null)
return;
let tagGroups = new Set(SavedSearchTags.getAllGroups().keys());
if (tagGroups.has(label)) {
ppixiv.message.show(\`Group "\${label}" already exists\`);
return;
}
SavedSearchTags.add(null, { group: label });
await this._populateDropdown();
let newSection = this._getSectionHeaderForGroup(label);
this._scrollEntryIntoView(newSection);
return;
}
let tagButton = e.target.closest("a[data-tag]");
if (tagButton) {
if (this.editing) {
e.preventDefault();
} else {
this._inputElement.blur();
this.hide();
this._inputElement.value = entry.dataset.tag;
return;
}
}
if (this.editing) {
let moveGroupUp = e.target.closest(".move-group-up");
let moveGroupDown = e.target.closest(".move-group-down");
if (moveGroupUp || moveGroupDown) {
e.stopPropagation();
e.preventDefault();
SavedSearchTags.moveGroup(tagSection.groupName, { down: moveGroupDown != null });
return;
}
let saveSearch = e.target.closest(".save-search");
if (saveSearch) {
e.stopPropagation();
e.preventDefault();
let tagGroups = new Set(SavedSearchTags.getAllGroups().keys());
tagGroups.delete(null);
let addToGroup = "Saved tags";
if (tagGroups.size == 1)
addToGroup = Array.from(tagGroups)[0];
else if (tagGroups.size > 1) {
let allGroups = new Set(SavedSearchTags.getAllGroups().keys());
allGroups.delete(null);
let collapsedGroups = SavedSearchTags.getCollapsedTagGroups();
addToGroup = null;
for (let group of allGroups) {
if (collapsedGroups.has(group))
continue;
addToGroup = group;
}
if (addToGroup == null) {
for (let group of allGroups)
addToGroup = group;
}
}
console.log(\`Adding search "\${entry.dataset.tag}" to group "\${addToGroup}"\`);
if (SavedSearchTags.getCollapsedTagGroups().has(addToGroup)) {
console.log(\`Uncollapsing group \${addToGroup} because we're adding to it\`);
SavedSearchTags.setTagGroupCollapsed(addToGroup, false);
}
SavedSearchTags.add(entry.dataset.tag, { group: addToGroup, addToEnd: true });
await this._populateDropdown();
let newEntry = this.getEntryForTag(entry.dataset.tag);
this._scrollEntryIntoView(newEntry);
}
let editTags = e.target.closest(".edit-tags-button");
if (editTags != null) {
e.stopPropagation();
e.preventDefault();
let newTags = await this.textPrompt({ title: "Edit search:", value: entry.dataset.tag + " " });
if (newTags == null || newTags == entry.dataset.tag)
return;
newTags = newTags.trim();
SavedSearchTags.modifyTag(entry.dataset.tag, newTags);
return;
}
let removeEntry = e.target.closest(".delete-entry");
if (removeEntry != null) {
e.stopPropagation();
e.preventDefault();
if (entry != null) {
SavedSearchTags.remove(entry.dataset.tag);
return;
}
let tagsInGroup = SavedSearchTags.getAllGroups().get(tagSection.groupName);
if (tagsInGroup.length > 0) {
let header, text = null;
if (tagSection.groupName == null)
header = \`Clear \${tagsInGroup.length} recent \${tagsInGroup.length == 1 ? "search" : "searches"}?\`;
else {
header = "Delete tag group";
text = \`This group contains \${tagsInGroup.length} \${tagsInGroup.length == 1 ? "tag" : "tags"}.
Delete this group and all tags inside it? This can't be undone.\`;
}
let result = await this.parent.confirmPrompt({ header, text });
if (!result)
return;
}
console.log("Deleting group:", tagSection.groupName);
console.log("Containing tags:", tagsInGroup);
SavedSearchTags.deleteGroup(tagSection.groupName);
return;
}
let renameGroup = e.target.closest(".rename-group-button");
if (renameGroup != null) {
e.stopPropagation();
e.preventDefault();
if (tagSection.groupName == null)
return;
let newGroupName = await this.textPrompt({ title: "Rename group:", value: tagSection.groupName });
if (newGroupName == null || newGroupName == tagSection.groupName)
return;
SavedSearchTags.renameGroup(tagSection.groupName, newGroupName);
return;
}
}
if (tagSection != null && !tagSection.classList.contains("autocomplete")) {
e.stopPropagation();
e.preventDefault();
SavedSearchTags.setTagGroupCollapsed(tagSection.groupName, "toggle");
return;
}
};
_inputKeydown = (e) => {
if (this.root.hidden)
return;
switch (e.code) {
case "ArrowUp":
case "ArrowDown":
e.preventDefault();
e.stopImmediatePropagation();
this.move(e.code == "ArrowDown");
break;
}
};
_inputSelectionChange = (e) => {
this._runAutocomplete();
};
inputOnInput = (e) => {
if (this.root.hidden)
return;
this.setSelection(null);
this._runAutocomplete();
};
async _load() {
this.root.classList.add("loading");
this.root.hidden = false;
if (!await this._populateDropdown())
return;
this._selectCurrentSearch();
this._runAutocomplete();
}
hide() {
if (!this.visible)
return;
this.visible = false;
this._cancelPopulateDropdown();
this._currentAutocompleteResults = {};
this._mostRecentAutocomplete = null;
this.editing = false;
this.dragger.cancelDrag();
this.root.hidden = true;
}
async _runAutocomplete() {
if (!this.visible)
return;
if (this.navigating)
return;
if (this._disableAutocompleteUntil > Date.now())
return;
let tags = this._inputElement.value.trim();
let text = this._inputElement.value;
let wordStart = this._inputElement.selectionStart;
while (wordStart > 0 && text[wordStart - 1] != " ")
wordStart--;
let wordEnd = this._inputElement.selectionEnd;
while (wordEnd < text.length && text[wordEnd] != " ")
wordEnd++;
let keyword;
if (this._inputElement.selectionStart != this._inputElement.selectionEnd)
keyword = text.substr(this._inputElement.selectionStart, this._inputElement.selectionEnd - this._inputElement.selectionStart);
else
keyword = text.substr(wordStart, wordEnd - wordStart);
keyword = keyword.trim();
keyword = keyword.replace(/ .*/, "");
keyword = keyword.replace(/^\\(+/g, "");
keyword = keyword.replace(/\\)+\$/g, "");
if (keyword == "or")
return;
if (this._mostRecentAutocomplete == keyword)
return;
if (this._abortAutocomplete != null) {
return;
}
this._mostRecentAutocomplete = keyword;
let cachedResult = this._autocompleteCache.get(keyword);
if (cachedResult != null) {
this._autocompleteRequestFinished(tags, keyword, { candidates: cachedResult, text, wordStart, wordEnd });
return;
}
if (keyword == "") {
if (this._abortAutocomplete != null)
this._abortAutocomplete.abort();
this._autocompleteRequestFinished(tags, keyword, { candidates: [] });
return;
}
let result = null;
try {
this._abortAutocomplete = new AbortController();
result = await helpers.pixivRequest.get("/rpc/cps.php", {
keyword
}, {
signal: this._abortAutocomplete.signal
});
} catch (e) {
console.info("Tag autocomplete error:", e);
return;
} finally {
this._abortAutocomplete = null;
}
if (result == null)
return;
this._autocompleteRequestFinished(tags, keyword, { candidates: result.candidates, text, wordStart, wordEnd });
}
_autocompleteRequestFinished(tags, word, { candidates, text, wordStart, wordEnd } = {}) {
this._abortAutocomplete = null;
this._autocompleteCache.set(word, candidates);
let translations = {};
for (let tag of candidates) {
if (tag.type != "tag_translation")
continue;
if (helpers.strings.containsAsianText(tag.tag_translation)) {
continue;
}
translations[tag.tag_name] = {
en: tag.tag_translation
};
}
ppixiv.tagTranslations.addTranslationsDict(translations, { overwrite: false });
this._currentAutocompleteResults = {
autocompletedTags: [],
text,
wordStart,
wordEnd
};
for (let candidate of candidates || []) {
if (candidate.tag_name == word)
continue;
this._currentAutocompleteResults.autocompletedTags.push({
tag: candidate.tag_name,
accessCount: candidate.access_count,
tagTranslation: candidate.tag_translation
});
}
this._populateDropdown();
if (tags != this._inputElement.value)
this._runAutocomplete();
}
createEntry(tags, { classes, targetTags = null } = {}) {
let entry = this.createTemplate({ name: "tag-dropdown-entry", html: \`
<a class=entry href=#>
<div class="edit-button drag-handle" data-shown-in="saved">
\${helpers.createIcon("mat:drag_handle")}
</div>
<div class="edit-button save-search" data-shown-in="recent autocomplete">
\${helpers.createIcon("mat:push_pin")}
</div>
<span class=search></span>
<span class="edit-button edit-tags-button" data-shown-in="saved">\${helpers.createIcon("mat:edit")}</span>
<span class="edit-button delete-entry" data-shown-in="recent saved">X</span>
</a>
\` });
targetTags ??= tags;
entry.dataset.tag = targetTags;
for (let name of classes)
entry.classList.add(name);
let translatedTag = this.translatedTags[tags];
if (translatedTag)
entry.dataset.translatedTag = translatedTag;
let tagContainer = entry.querySelector(".search");
for (let tag of helpers.pixiv.splitSearchTags(tags)) {
if (tag == "")
continue;
if (tag.toLowerCase() == "or")
tag = "or";
let span = document.createElement("span");
span.dataset.tag = tag;
span.classList.add("word");
if (tag == "or")
span.classList.add("or");
else
span.classList.add("tag");
let prefixAndTag = helpers.pixiv.splitTagPrefixes(tag);
let translatedTag2 = this.translatedTags[prefixAndTag[1]];
if (translatedTag2)
translatedTag2 = prefixAndTag[0] + translatedTag2;
span.textContent = translatedTag2 || tag;
if (translatedTag2)
span.dataset.translatedTag = translatedTag2;
tagContainer.appendChild(span);
}
let url = helpers.getArgsForTagSearch(targetTags, ppixiv.plocation);
entry.href = url;
return entry;
}
createSeparator(label, {
icon,
isUserSection,
groupName = null,
collapsed = false,
classes = [],
isAutocompleteHeader = false
}) {
let section = this.createTemplate({ html: \`
<div class=tag-section>
<div class="edit-button user-section-edit-button move-group-up">
\${helpers.createIcon("mat:arrow_upward")}
</div>
<div class="edit-button user-section-edit-button move-group-down">
\${helpers.createIcon("mat:arrow_downward")}
</div>
\${helpers.createIcon(icon, { classes: ["section-icon"] })}
<span class=label></span>
<span class="edit-button rename-group-button">\${helpers.createIcon("mat:edit")}</span>
<span class="edit-button delete-entry">X</span>
<span class="group-suggestions-button" hidden>\${helpers.createIcon("mat:unfold_less")}</span>
</div>
\` });
section.querySelector(".label").textContent = label;
helpers.html.setClass(section, "user-section", isUserSection);
helpers.html.setClass(section, "collapsed", collapsed);
if (groupName != null)
section.dataset.group = groupName;
else
section.classList.add("recents");
section.groupName = groupName;
if (groupName == null)
section.querySelector(".rename-group-button").hidden = true;
for (let name of classes)
section.classList.add(name);
let groupSuggestionsButton = section.querySelector(".group-suggestions-button");
groupSuggestionsButton.hidden = !isAutocompleteHeader;
groupSuggestionsButton.classList.toggle("selected", ppixiv.settings.get("collapse_autocomplete"));
groupSuggestionsButton.addEventListener("click", () => {
ppixiv.settings.set("collapse_autocomplete", !ppixiv.settings.get("collapse_autocomplete"));
this._populateDropdown();
});
return section;
}
move(down) {
this.navigating = true;
try {
let allEntries = this._allResults.querySelectorAll(".entry");
let totalEntries = allEntries.length;
if (totalEntries == 0)
return;
let selectedIdx = null;
for (let idx = 0; idx < allEntries.length; ++idx) {
if (allEntries[idx].classList.contains("selected")) {
selectedIdx = idx;
break;
}
}
if (selectedIdx == null)
selectedIdx = down ? 0 : totalEntries - 1;
else
selectedIdx += down ? 1 : -1;
selectedIdx = (selectedIdx + totalEntries) % totalEntries;
if (this._abortAutocomplete != null)
this._abortAutocomplete.abort();
let newEntry = allEntries[selectedIdx];
this.setSelection(newEntry.dataset.tag);
this._disableAutocompleteUntil = Date.now() + 50;
this._inputElement.value = newEntry.dataset.tag;
} finally {
this.navigating = false;
}
}
getSelection() {
let entry = this._allResults.querySelector(".entry.selected");
return entry?.dataset?.tag;
}
setSelection(tags) {
this.navigating = true;
try {
let oldSelection = this._allResults.querySelector(".entry.selected");
if (oldSelection)
oldSelection.classList.remove("selected");
if (tags != null) {
let entry = this.getEntryForTag(tags, { includeAutocomplete: true });
if (entry) {
entry.classList.add("selected");
this._scrollEntryIntoView(entry);
}
}
} finally {
this.navigating = false;
}
}
_selectCurrentSearch = () => {
let currentSearchTags = this._inputElement.value.trim();
if (!currentSearchTags)
return;
this.setSelection(currentSearchTags);
let selectedEntry = this.root.querySelector(".entry.selected");
if (selectedEntry)
this._scrollEntryIntoView(selectedEntry);
};
_populateDropdown = async () => {
let promise = this._populateDropdownPromise = this._populateDropdownInner();
this._populateDropdownPromise.finally(() => {
if (promise === this._populateDropdownPromise)
this._populateDropdownPromise = null;
});
while (this.visible && this._populateDropdownPromise != null) {
if (await this._populateDropdownPromise)
return true;
}
return false;
};
_populateDropdownInner = async () => {
this._cancelPopulateDropdown();
let abortController = this._populateDropdownAbort = new AbortController();
let abortSignal = abortController.signal;
let { autocompletedTags = [], text, wordStart, wordEnd } = this._currentAutocompleteResults;
let tagsByGroup = SavedSearchTags.getAllGroups();
let allSavedTags = [];
for (let savedTag of tagsByGroup.values())
allSavedTags = [...allSavedTags, ...savedTag];
for (let tag of autocompletedTags)
allSavedTags.push(tag.tag);
let allTags = {};
for (let tagSearch of allSavedTags) {
for (let tag of helpers.pixiv.splitSearchTags(tagSearch)) {
tag = helpers.pixiv.splitTagPrefixes(tag)[1];
allTags[tag] = true;
}
}
allTags = Object.keys(allTags);
let translatedTags;
if (this.draggingTag == null) {
translatedTags = await ppixiv.tagTranslations.getTranslations(allTags, "en");
if (abortSignal.aborted)
return false;
this.translatedTags = translatedTags;
}
let savedSelection = this.getSelection();
let savedPosition = this.savedPosition ?? this._saveSearchPosition();
this.savedPosition = null;
savedPosition ??= {};
helpers.html.removeElements(this._inputDropdownContents);
if (autocompletedTags.length) {
let separator = this.createSeparator(\`Suggestions for \${this._mostRecentAutocomplete}\`, {
icon: "mat:assistant",
classes: ["autocomplete"],
isAutocompleteHeader: true
});
this._inputDropdownContents.appendChild(separator);
}
if (ppixiv.settings.get("collapse_autocomplete"))
autocompletedTags = helpers.pixiv.groupTagsByTranslation(autocompletedTags, translatedTags);
for (let { tag, forTag, tagList } of autocompletedTags) {
let search = text.slice(0, wordStart) + tag + text.slice(wordEnd);
if (tagList && tagList.length > 1)
tag = \`\${forTag} (\${tagList.join(", ")})\`;
let entry = this.createEntry(tag, {
classes: ["autocomplete"],
targetTags: search
});
this._inputDropdownContents.appendChild(entry);
}
for (let [groupName, tagsInGroup] of tagsByGroup.entries()) {
if (groupName == null)
continue;
let collapsed = SavedSearchTags.getCollapsedTagGroups().has(groupName);
this._inputDropdownContents.appendChild(this.createSeparator(groupName, {
icon: collapsed ? "mat:folder" : "mat:folder_open",
isUserSection: true,
groupName,
collapsed
}));
if (!collapsed) {
for (let tag of tagsInGroup)
this._inputDropdownContents.appendChild(this.createEntry(tag, { classes: ["history", "saved"] }));
}
}
let recentsCollapsed = SavedSearchTags.getCollapsedTagGroups().has(null);
let recentTags = tagsByGroup.get(null);
if (recentTags.length)
this._inputDropdownContents.appendChild(this.createSeparator("Recent tags", {
icon: "mat:history",
collapsed: recentsCollapsed
}));
if (!recentsCollapsed) {
for (let tag of recentTags)
this._inputDropdownContents.appendChild(this.createEntry(tag, { classes: ["history", "recent"] }));
}
if (savedSelection)
this.setSelection(savedSelection);
this._restoreSearchPosition(savedPosition);
let empty = Array.from(this._allResults.querySelectorAll(".entry, .tag-section")).length == 0;
helpers.html.setClass(this.root, "loading", empty);
return true;
};
_cancelPopulateDropdown() {
if (this._populateDropdownAbort == null)
return;
this._populateDropdownAbort.abort();
}
_saveSearchPosition() {
let ignoredNodes = /* @__PURE__ */ new Set();
if (this.draggingTag) {
let entry = this._findTagEntry(this.draggingTag);
ignoredNodes.add(entry);
let nextEntry = entry.nextElementSibling;
if (nextEntry)
ignoredNodes.add(nextEntry);
let previousEntry = entry.previousElementSibling;
if (previousEntry)
ignoredNodes.add(previousEntry);
}
for (let node of this._inputDropdown.querySelectorAll(".entry[data-tag]")) {
if (node.offsetTop < this.root.scrollTop)
continue;
if (ignoredNodes.has(node))
continue;
let savedPosition = helpers.html.saveScrollPosition(this.root, node);
let tag = node.dataset.tag;
return { savedPosition, tag };
}
return {};
}
_restoreSearchPosition({ savedPosition, tag }) {
if (savedPosition == null)
return;
let restoreEntry = this.getEntryForTag(tag);
if (restoreEntry)
helpers.html.restoreScrollPosition(this.root, restoreEntry, savedPosition);
}
_scrollEntryIntoView(entry) {
entry.scrollIntoView({ block: "nearest" });
if (!entry.classList.contains("entry"))
return;
let stickyTop = null;
for (let node of this._inputDropdownContents.children) {
if (!node.classList.contains("tag-section"))
continue;
if (stickyTop != null && node.offsetTop != stickyTop.offsetTop)
break;
stickyTop = node;
}
let stickyPadding = stickyTop.offsetHeight;
let offsetFromTop = entry.offsetTop - this._inputDropdown.offsetTop - this.root.scrollTop;
if (offsetFromTop < stickyPadding)
this.root.scrollTop -= stickyPadding - offsetFromTop;
}
};
var RelatedTagDropdown = class extends TagDropdownWidget {
async refreshTags() {
let tags = this.dataSource.relatedTags;
if (tags == null)
return;
if (this._currentTags != null && JSON.stringify(this._currentTags) == JSON.stringify(tags))
return;
let tagList = tags;
let translatedTags = await ppixiv.tagTranslations.getTranslations(tagList, "en");
if (tagList != tags)
return;
this._currentTags = tags;
helpers.html.removeElements(this.root);
for (let tag of tagList) {
let translatedTag = tag;
if (translatedTags[tag])
translatedTag = translatedTags[tag];
let a = helpers.createBoxLink({
label: translatedTag,
classes: ["tag-entry"],
link: this.formatTagLink(tag),
asElement: true
});
this.root.appendChild(a);
a.dataset.tag = tag;
}
}
formatTagLink(tag) {
return helpers.getArgsForTagSearch(tag, ppixiv.plocation);
}
};
/\x2f web/vview/sites/pixiv/data-sources/search-illusts.js
var DataSource_Search = class extends DataSource {
get name() {
return "search";
}
get ui() {
return UI7;
}
constructor(args) {
super(args);
let tag = this._searchTags;
if (tag)
SavedSearchTags.add(tag);
this.cacheSearchTitle();
}
get supportsStartPage() {
return true;
}
get hasNoResults() {
if (!this._searchTags)
return false;
return super.hasNoResults;
}
get _searchTags() {
return helpers.pixiv.getSearchTagsFromUrl(this.url);
}
get _searchType() {
let url = helpers.pixiv.getUrlWithoutLanguage(this.url);
let parts = url.pathname.split("/");
if (parts.length >= 4)
return parts[3];
else
return "artworks";
}
startup() {
super.startup();
ppixiv.settings.addEventListener("disable-translations", this.cacheSearchTitle);
}
shutdown() {
super.shutdown();
ppixiv.settings.removeEventListener("disable-translations", this.cacheSearchTitle);
}
cacheSearchTitle = async () => {
this.title = "Search: ";
let tags = this._searchTags;
if (tags) {
tags = await ppixiv.tagTranslations.translateTagList(tags, "en");
let tagList = document.createElement("vv-container");
for (let tag of tags) {
if (tag.toLowerCase() == "or")
tag = "or";
let span = document.createElement("span");
span.innerText = tag;
span.classList.add("word");
if (tag == "or")
span.classList.add("or");
else if (tag == "(" || tag == ")")
span.classList.add("paren");
else
span.classList.add("tag");
tagList.appendChild(span);
}
this.title += tags.join(" ");
this.displayingTags = tagList;
}
this.callUpdateListeners();
};
async loadPageInternal(page) {
let args = {};
this.url.searchParams.forEach((value, key) => {
args[key] = value;
});
args.p = page;
let searchType = this._searchType;
let searchMode = this.getUrlSearchMode();
let apiSearchType = null;
if (searchMode == "all") {
apiSearchType = "artworks";
} else if (searchMode == "illust") {
apiSearchType = "illustrations";
args.type = "illust_and_ugoira";
} else if (searchMode == "manga") {
apiSearchType = "manga";
args.type = "manga";
} else if (searchMode == "ugoira") {
apiSearchType = "illustrations";
args.type = "ugoira";
} else
console.error("Invalid search type:", searchType);
let tag = this._searchTags;
if (!tag) {
console.log("No search tags");
return;
}
let url = "/ajax/search/" + apiSearchType + "/" + encodeURIComponent(tag);
let result = await helpers.pixivRequest.get(url, args);
let body = result.body;
if (this.relatedTags == null) {
this.relatedTags = body.relatedTags;
this.callUpdateListeners();
}
let translations = [];
for (let tag2 of Object.keys(body.tagTranslation)) {
translations.push({
tag: tag2,
translation: body.tagTranslation[tag2]
});
}
ppixiv.tagTranslations.addTranslations(translations);
let illusts = body.illust || body.illustManga || body.manga;
illusts = illusts.data;
let mediaIds = await ppixiv.mediaCache.addMediaInfosPartial(illusts, "normal");
return { mediaIds };
}
get pageTitle() {
return this.title;
}
getDisplayingText() {
return this.displayingTags ?? "Search works";
}
getUrlSearchMode() {
let searchType = this._searchType;
if (searchType == "illustrations") {
let querySearchType = this.url.searchParams.get("type");
if (querySearchType == "ugoira")
return "ugoira";
if (querySearchType == "illust")
return "illust";
return "all";
}
if (searchType == "artworks")
return "all";
if (searchType == "manga")
return "manga";
return "all";
}
setUrlSearchMode(url, mode) {
url = new URL(url);
url = helpers.pixiv.getUrlWithoutLanguage(url);
if (mode == "illust")
url.searchParams.set("type", "illust");
else if (mode == "ugoira")
url.searchParams.set("type", "ugoira");
else
url.searchParams.delete("type");
let searchType = "artworks";
if (mode == "manga")
searchType = "manga";
else if (mode == "ugoira" || mode == "illust")
searchType = "illustrations";
let parts = url.pathname.split("/");
parts[3] = searchType;
url.pathname = parts.join("/");
return url;
}
};
var UI7 = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, template: \`
<div>
<div class=tag-search-with-related-tags>
<vv-container class=tag-search-box-container></vv-container>
</div>
<div class=box-button-row>
\${helpers.createBoxLink({ label: "Ages", classes: ["ages-button"] })}
\${helpers.createBoxLink({ label: "Sort", classes: ["sort-button"] })}
\${helpers.createBoxLink({ label: "Type", classes: [["search-type-button"]] })}
\${helpers.createBoxLink({ label: "Search mode", classes: ["search-mode-button"] })}
\${helpers.createBoxLink({ label: "Image size", classes: ["image-size-button"] })}
\${helpers.createBoxLink({ label: "Aspect ratio", classes: ["aspect-ratio-button"] })}
\${helpers.createBoxLink({ label: "Bookmarks", classes: ["bookmark-count-button", "premium-only"] })}
\${helpers.createBoxLink({ label: "Time", classes: ["time-ago-button"] })}
\${helpers.createBoxLink({ label: "Hide AI", popup: "Show only R18 works", dataType: "hide-ai" })}
\${helpers.createBoxLink({ label: "Reset", popup: "Clear all search options", classes: ["reset-search"] })}
</div>
</div>
\` });
this.dataSource = dataSource;
this.dataSource.addEventListener("updated", () => this.refresh(), this._signal);
dataSource.setupDropdown(this.querySelector(".ages-button"), [{
createOptions: { label: "All", dataset: { default: true } },
setupOptions: { fields: { mode: null } }
}, {
createOptions: { label: "All ages" },
setupOptions: { fields: { mode: "safe" } }
}, {
createOptions: { label: "R18", classes: ["r18"] },
setupOptions: { fields: { mode: "r18" } }
}]);
dataSource.setupDropdown(this.querySelector(".sort-button"), [{
createOptions: { label: "Newest", dataset: { default: true } },
setupOptions: { fields: { order: null }, defaults: { order: "date_d" } }
}, {
createOptions: { label: "Oldest" },
setupOptions: { fields: { order: "date" } }
}, {
createOptions: { label: "Popularity", classes: ["premium-only"] },
setupOptions: { fields: { order: "popular_d" } }
}, {
createOptions: { label: "Popular with men", classes: ["premium-only"] },
setupOptions: { fields: { order: "popular_male_d" } }
}, {
createOptions: { label: "Popular with women", classes: ["premium-only"] },
setupOptions: { fields: { order: "popular_female_d" } }
}]);
let urlFormat = "tags/tag/type";
dataSource.setupDropdown(this.querySelector(".search-type-button"), [{
createOptions: { label: "All", dataset: { default: true } },
setupOptions: {
urlFormat,
fields: { "/type": "artworks", type: null }
}
}, {
createOptions: { label: "Illustrations" },
setupOptions: {
urlFormat,
fields: { "/type": "illustrations", type: "illust" }
}
}, {
createOptions: { label: "Manga" },
setupOptions: {
urlFormat,
fields: { "/type": "manga", type: null }
}
}, {
createOptions: { label: "Animations" },
setupOptions: {
urlFormat,
fields: { "/type": "illustrations", type: "ugoira" }
}
}]);
dataSource.setItem(this.root, {
type: "hide-ai",
toggle: true,
fields: { ai_type: "1" }
});
if (ppixiv.pixivInfo.hideAiWorks)
this.root.querySelector(\`[data-type='hide-ai']\`).hidden = true;
dataSource.setupDropdown(this.querySelector(".search-mode-button"), [{
createOptions: { label: "Tag", dataset: { default: true } },
setupOptions: { fields: { s_mode: null }, defaults: { s_mode: "s_tag" } }
}, {
createOptions: { label: "Exact tag match" },
setupOptions: { fields: { s_mode: "s_tag_full" } }
}, {
createOptions: { label: "Text search" },
setupOptions: { fields: { s_mode: "s_tc" } }
}]);
dataSource.setupDropdown(this.querySelector(".image-size-button"), [{
createOptions: { label: "All", dataset: { default: true } },
setupOptions: { fields: { wlt: null, hlt: null, wgt: null, hgt: null } }
}, {
createOptions: { label: "High-res" },
setupOptions: { fields: { wlt: 3e3, hlt: 3e3, wgt: null, hgt: null } }
}, {
createOptions: { label: "Medium-res" },
setupOptions: { fields: { wlt: 1e3, hlt: 1e3, wgt: 2999, hgt: 2999 } }
}, {
createOptions: { label: "Low-res" },
setupOptions: { fields: { wlt: null, hlt: null, wgt: 999, hgt: 999 } }
}]);
dataSource.setupDropdown(this.querySelector(".aspect-ratio-button"), [{
createOptions: { label: "All", icon: "", dataset: { default: true } },
setupOptions: { fields: { ratio: null } }
}, {
createOptions: { label: "Landscape", icon: "panorama" },
setupOptions: { fields: { ratio: "0.5" } }
}, {
createOptions: { label: "Portrait", icon: "portrait" },
setupOptions: { fields: { ratio: "-0.5" } }
}, {
createOptions: { label: "Square", icon: "crop_square" },
setupOptions: { fields: { ratio: "0" } }
}]);
dataSource.setupDropdown(this.querySelector(".bookmark-count-button"), [{
createOptions: { label: "All", dataType: "bookmarks-all", dataset: { default: true } },
setupOptions: { fields: { blt: null, bgt: null } }
}, {
createOptions: { label: "100+", dataType: "bookmarks-100" },
setupOptions: { fields: { blt: 100, bgt: null } }
}, {
createOptions: { label: "250+", dataType: "bookmarks-250" },
setupOptions: { fields: { blt: 250, bgt: null } }
}, {
createOptions: { label: "500+", dataType: "bookmarks-500" },
setupOptions: { fields: { blt: 500, bgt: null } }
}, {
createOptions: { label: "1000+", dataType: "bookmarks-1000" },
setupOptions: { fields: { blt: 1e3, bgt: null } }
}, {
createOptions: { label: "2500+", dataType: "bookmarks-2500" },
setupOptions: { fields: { blt: 2500, bgt: null } }
}, {
createOptions: { label: "5000+", dataType: "bookmarks-5000" },
setupOptions: { fields: { blt: 5e3, bgt: null } }
}]);
new DropdownMenuOpener({
button: this.querySelector(".time-ago-button"),
createDropdown: ({ ...options2 }) => {
let dropdown = new Widget({
...options2,
template: \`
<div class=vertical-list>
\${helpers.createBoxLink({ label: "All", dataType: "time-all", dataset: { default: true } })}
\${helpers.createBoxLink({ label: "This week", dataType: "time-week", dataset: { shortLabel: "Weekly" } })}
\${helpers.createBoxLink({ label: "This month", dataType: "time-month" })}
\${helpers.createBoxLink({ label: "This year", dataType: "time-year" })}
<div class=years-ago>
\${helpers.createBoxLink({ label: "1", dataType: "time-years-ago-1", dataset: { shortLabel: "1 year" } })}
\${helpers.createBoxLink({ label: "2", dataType: "time-years-ago-2", dataset: { shortLabel: "2 years" } })}
\${helpers.createBoxLink({ label: "3", dataType: "time-years-ago-3", dataset: { shortLabel: "3 years" } })}
\${helpers.createBoxLink({ label: "4", dataType: "time-years-ago-4", dataset: { shortLabel: "4 years" } })}
\${helpers.createBoxLink({ label: "5", dataType: "time-years-ago-5", dataset: { shortLabel: "5 years" } })}
\${helpers.createBoxLink({ label: "6", dataType: "time-years-ago-6", dataset: { shortLabel: "6 years" } })}
\${helpers.createBoxLink({ label: "7", dataType: "time-years-ago-7", dataset: { shortLabel: "7 years" } })}
<span>years ago</span>
</div>
</div>
\`
});
dataSource.setItem(dropdown, { type: "time-all", fields: { scd: null, ecd: null } });
let formatDate = (date) => {
return (date.getYear() + 1900).toFixed().padStart(2, "0") + "-" + (date.getMonth() + 1).toFixed().padStart(2, "0") + "-" + date.getDate().toFixed().padStart(2, "0");
};
let setDateFilter = (name, start, end) => {
let startDate = formatDate(start);
let endDate = formatDate(end);
dataSource.setItem(dropdown, { type: name, fields: { scd: startDate, ecd: endDate } });
};
let tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
let lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
let lastMonth = new Date();
lastMonth.setMonth(lastMonth.getMonth() - 1);
let lastYear = new Date();
lastYear.setFullYear(lastYear.getFullYear() - 1);
setDateFilter("time-week", lastWeek, tomorrow);
setDateFilter("time-month", lastMonth, tomorrow);
setDateFilter("time-year", lastYear, tomorrow);
for (let yearsAgo = 1; yearsAgo <= 7; ++yearsAgo) {
let startYear = new Date();
startYear.setFullYear(startYear.getFullYear() - yearsAgo - 1);
let endYear = new Date();
endYear.setFullYear(endYear.getFullYear() - yearsAgo);
setDateFilter("time-years-ago-" + yearsAgo, startYear, endYear);
}
let box = this.querySelector(".reset-search");
let url = new URL(this.dataSource.url);
let tag = helpers.pixiv.getSearchTagsFromUrl(url);
url.search = "";
if (tag == null)
url.pathname = "/tags";
else
url.pathname = "/tags/" + encodeURIComponent(tag) + "/artworks";
box.href = url;
return dropdown;
}
});
this.tagSearchBox = new TagSearchBoxWidget({
container: this.querySelector(".tag-search-box-container"),
dataSource: this.dataSource
});
let search = this.dataSource._searchTags;
if (search)
search += " ";
this.querySelector(".tag-search-box .input-field-container > input").value = search;
}
};
/\x2f web/vview/sites/pixiv/data-sources/new-posts-by-following.js
var DataSource_NewPostsByFollowing = class extends DataSource {
get name() {
return "new_works_by_following";
}
get pageTitle() {
return "Following";
}
getDisplayingText() {
return "Following";
}
get ui() {
return UI8;
}
constructor(args) {
super(args);
this.bookmarkTags = [];
}
get supportsStartPage() {
return true;
}
async loadPageInternal(page) {
let currentTag = this.url.searchParams.get("tag") || "";
let r18 = this.url.pathname == "/bookmark_new_illust_r18.php";
let result = await helpers.pixivRequest.get("/ajax/follow_latest/illust", {
p: page,
tag: currentTag,
mode: r18 ? "r18" : "all"
});
let data = result.body;
ppixiv.tagTranslations.addTranslationsDict(data.tagTranslation);
this.bookmarkTags = data.page.tags;
this.bookmarkTags.sort((lhs, rhs) => lhs.toLowerCase().localeCompare(rhs.toLowerCase()));
this.callUpdateListeners();
await ppixiv.mediaCache.addMediaInfosPartial(data.thumbnails.illust, "normal");
let mediaIds = [];
for (let illust of data.thumbnails.illust)
mediaIds.push(helpers.mediaId.fromIllustId(illust.id));
return { mediaIds };
}
};
var UI8 = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, template: \`
<div>
<div class=box-button-row>
\${helpers.createBoxLink({ label: "R18", popup: "Show only R18 works", dataType: "bookmarks-new-illust-ages-r18", classes: ["r18"] })}
\${helpers.createBoxLink({ label: "All tags", popup: "Follow tags", icon: "bookmark", classes: ["follow-tag-button", "premium-only"] })}
</div>
</div>
\` });
this.dataSource = dataSource;
class FollowTagDropdown extends TagDropdownWidget {
refreshTags() {
let currentTag = dataSource.url.searchParams.get("tag") || "All tags";
for (let tag of this.root.querySelectorAll(".tag-entry"))
tag.remove();
this.addTagLink("All tags");
for (let tag of dataSource.bookmarkTags)
this.addTagLink(tag);
if (dataSource.bookmarkTags.length == 0 && currentTag != "All tags")
this.addTagLink(currentTag);
}
addTagLink(tag) {
if (tag == "null")
return;
let label = tag;
if (tag == "All tags")
tag = null;
let a = helpers.createBoxLink({
label,
classes: ["tag-entry"],
link: "#",
asElement: true,
dataType: "following-tag"
});
if (label == "All tags")
a.dataset.default = 1;
dataSource.setItem(a, { fields: { "tag": tag } });
this.root.appendChild(a);
}
}
;
new DropdownMenuOpener({
button: this.querySelector(".follow-tag-button"),
createDropdown: ({ ...options2 }) => new FollowTagDropdown({ dataSource, ...options2 })
});
dataSource.setItem(this.root, {
type: "bookmarks-new-illust-ages-r18",
toggle: true,
urlFormat: "path",
fields: { "/path": "bookmark_new_illust_r18.php" },
defaults: { "/path": "bookmark_new_illust.php" }
});
}
};
/\x2f web/vview/sites/pixiv/data-sources/new-posts-by-everyone.js
var DataSource_NewPostsByEveryone = class extends DataSource {
get name() {
return "new_illust";
}
get pageTitle() {
return "New Works";
}
getDisplayingText() {
return "New Works";
}
get ui() {
return UI9;
}
async loadPageInternal(page) {
let args = new helpers.args(this.url);
let r18 = this.url.pathname == "/new_illust_r18.php";
let type = args.query.get("type") || "illust";
if (this.lastId == null) {
this.lastId = 0;
this.lastIdPage = 1;
}
if (this.lastIdPage != page) {
console.error("Pages weren't loaded in order");
return;
}
console.log("Assuming page", page, "starts at", this.lastId);
let url = "/ajax/illust/new";
let result = await helpers.pixivRequest.get(url, {
limit: 20,
type,
r18,
lastId: this.lastId
});
if (result.body.illusts.length > 0) {
this.lastId = result.body.illusts[result.body.illusts.length - 1].id;
this.lastIdPage++;
}
let mediaIds = [];
for (let illustData of result.body.illusts)
mediaIds.push(helpers.mediaId.fromIllustId(illustData.id));
await ppixiv.mediaCache.addMediaInfosPartial(result.body.illusts, "normal");
return { mediaIds };
}
};
var UI9 = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, template: \`
<div>
<div class=box-button-row>
<div class=box-button-row>
\${helpers.createBoxLink({ label: "Illustrations", popup: "Show illustrations", dataType: "new-illust-type-illust" })}
\${helpers.createBoxLink({ label: "Manga", popup: "Show manga only", dataType: "new-illust-type-manga" })}
</div>
<div class=box-button-row>
\${helpers.createBoxLink({ label: "R18", popup: "Show only R18 works", dataType: "new-illust-ages-r18" })}
</div>
</div>
</div>
\` });
dataSource.setItem(this.root, { type: "new-illust-type-illust", fields: { type: null } });
dataSource.setItem(this.root, { type: "new-illust-type-manga", fields: { type: "manga" } });
dataSource.setItem(this.root, {
type: "new-illust-ages-r18",
toggle: true,
urlFormat: "path",
fields: { "/path": "new_illust_r18.php" },
defaults: { "/path": "new_illust.php" }
});
}
};
/\x2f web/vview/sites/pixiv/data-sources/related-favorites.js
var DataSource_RelatedFavorites = class extends DataSourceFromPage {
get name() {
return "illust-bookmarks";
}
get pageTitle() {
return "Similar Bookmarks";
}
getDisplayingText() {
if (this.illustInfo)
return "Users who bookmarked " + this.illustInfo.illustTitle;
else
return "Users who bookmarked image";
}
constructor(args) {
super(args);
this.illustInfo = null;
}
async loadPageInternal(page) {
let queryArgs = this.url.searchParams;
let illustId = queryArgs.get("illustId");
let mediaId = helpers.mediaId.fromIllustId(illustId);
this.illustInfo = await ppixiv.mediaCache.getMediaInfo(mediaId);
this.callUpdateListeners();
return super.loadPageInternal(page);
}
parseDocument(doc) {
let ids = [];
for (let element of doc.querySelectorAll("li.bookmark-item a[data-user_id]")) {
ppixiv.extraCache.addQuickUserData({
user_id: element.dataset.user_id,
user_name: element.dataset.user_name,
profile_img: element.dataset.profile_img.replace("_50.", "_170.")
}, "users_bookmarking_illust");
ids.push("bookmarks:" + element.dataset.user_id);
}
return ids;
}
get uiInfo() {
let imageUrl = null;
let imageLinkUrl = null;
if (this.illustInfo) {
imageLinkUrl = \`/artworks/\${this.illustInfo.id}#ppixiv\`;
imageUrl = this.illustInfo.previewUrls[0];
}
return { imageUrl, imageLinkUrl };
}
};
/\x2f web/vview/sites/pixiv/data-sources/search-users.js
var DataSource_SearchUsers = class extends DataSource {
get name() {
return "search-users";
}
get allowExpandingMangaPages() {
return false;
}
async loadPageInternal(page) {
if (!this.username)
return;
if (page > 1)
return;
let result = await helpers.pixivRequest.get("/touch/ajax/search/users", {
nick: this.username,
s_mode: "s_usr",
p: page,
lang: "en"
});
if (result.error) {
ppixiv.message.show("Error reading search: " + result.message);
return;
}
let mediaIds = [];
for (let user of result.body.users) {
ppixiv.extraCache.addQuickUserData({
userId: user.user_id,
userName: user.user_name,
profileImageUrl: user.profile_img.main
});
mediaIds.push(\`user:\${user.user_id}\`);
}
return { mediaIds };
}
get username() {
return this.url.searchParams.get("nick") ?? "";
}
get ui() {
return UI10;
}
get hasNoResults() {
if (!this.username)
return false;
return super.hasNoResults;
}
get pageTitle() {
let search = this.username;
if (search)
return "Search users: " + search;
else
return "Search users";
}
getDisplayingText() {
return this.pageTitle;
}
};
var UI10 = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, template: \`
<div class="search-box">
<div class="user-search-box input-field-container hover-menu-box">
<input class=search-users placeholder="Search users">
<span class="search-submit-button right-side-button">
\${helpers.createIcon("search")}
</span>
</div>
</div>
\` });
this.dataSource = dataSource;
this.querySelector(".user-search-box .search-submit-button").addEventListener("click", this.submitUserSearch);
helpers.inputHandler(this.querySelector(".user-search-box input.search-users"), this.submitUserSearch);
this.querySelector(".search-users").value = dataSource.username;
}
submitUserSearch = (e) => {
let search = this.querySelector(".user-search-box input.search-users").value;
let url = new URL("/search_user.php#ppixiv", ppixiv.plocation);
url.searchParams.append("nick", search);
url.searchParams.append("s_mode", "s_usr");
helpers.navigate(url);
};
};
/\x2f web/vview/sites/pixiv/data-sources/completed-requests.js
var DataSources_CompletedRequests = class extends DataSource {
get name() {
return "completed-requests";
}
get pageTitle() {
return "Completed requests";
}
getDisplayingText() {
return "Completed requests";
}
get ui() {
return UI11;
}
get supportsStartPage() {
return true;
}
async loadPageInternal(page) {
let args = new helpers.args(new URL(this.url));
let showing = args.get("type") || "latest";
let mode = args.get("mode") || "all";
let type = args.getPathnameSegment(2);
let result = await helpers.pixivRequest.get(\`/ajax/commission/page/request/complete/\${type}\`, {
mode,
p: page,
lang: "en"
});
let request_data = {};
for (let request of result.body.requests)
request_data[request.requestId] = request;
for (let user of result.body.users)
ppixiv.userCache.addUserData(user);
await ppixiv.mediaCache.addMediaInfosPartial(result.body.thumbnails.illust, "normal");
ppixiv.tagTranslations.addTranslationsDict(result.body.tagTranslation);
let mediaIds = [];
let requestIds = result.body.page[showing == "latest" ? "requestIds" : "recommendRequestIds"];
if (requestIds == null)
return;
for (let requestId of requestIds) {
let request = request_data[requestId];
let request_post_id = request.postWork.postWorkId;
let mediaId = helpers.mediaId.fromIllustId(request_post_id);
if (ppixiv.mediaCache.getMediaInfoSync(mediaId, { full: false }) == null)
continue;
mediaIds.push(mediaId);
}
return { mediaIds };
}
};
var UI11 = class extends Widget {
constructor({ dataSource, ...options }) {
super({ ...options, template: \`
<div>
<div class="box-button-row group">
<div class=box-button-row>
\${helpers.createBoxLink({ label: "Latest", popup: "Show latest completed requests", dataType: "completed-requests-latest" })}
\${helpers.createBoxLink({ label: "Recommended", popup: "Show recommmended completed requests", dataType: "completed-requests-recommended" })}
</div>
<div class=box-button-row>
\${helpers.createBoxLink({ label: "Illustrations", popup: "Show latest completed requests", dataType: "completed-requests-illust" })}
\${helpers.createBoxLink({ label: "Animations", popup: "Show animations only", dataType: "completed-requests-ugoira" })}
\${helpers.createBoxLink({ label: "Manga", popup: "Show manga only", dataType: "completed-requests-manga" })}
</div>
<div class=box-button-row>
\${helpers.createBoxLink({ label: "All", popup: "Show all works", dataType: "completed-requests-all" })}
\${helpers.createBoxLink({ label: "All ages", popup: "Show all-ages works", dataType: "completed-requests-safe" })}
\${helpers.createBoxLink({ label: "R18", popup: "Show R18 works", dataType: "completed-requests-r18", classes: ["r18"] })}
</div>
</div>
</div>
\` });
dataSource.setItem(this.root, { type: "completed-requests-latest", fields: { type: "latest" }, defaults: { type: "latest" } });
dataSource.setItem(this.root, { type: "completed-requests-recommended", fields: { type: "recommended" }, defaults: { type: "latest" } });
dataSource.setItem(this.root, { type: "completed-requests-all", fields: { mode: "all" }, defaults: { mode: "all" } });
dataSource.setItem(this.root, { type: "completed-requests-safe", fields: { mode: "safe" }, defaults: { mode: "all" } });
dataSource.setItem(this.root, { type: "completed-requests-r18", fields: { mode: "r18" }, defaults: { mode: "all" } });
let urlFormat = "request/complete/type";
dataSource.setItem(this.root, { urlFormat, type: "completed-requests-illust", fields: { "/type": "illust" } });
dataSource.setItem(this.root, { urlFormat, type: "completed-requests-ugoira", fields: { "/type": "ugoira" } });
dataSource.setItem(this.root, { urlFormat, type: "completed-requests-manga", fields: { "/type": "manga" } });
}
};
/\x2f web/vview/sites/pixiv/data-sources/edited-images.js
var DataSources_EditedImages = class extends DataSourceFakePagination {
get name() {
return "edited";
}
get pageTitle() {
return "Edited";
}
getDisplayingText() {
return "Edited Images";
}
get allowExpandingMangaPages() {
return false;
}
async loadAllResults() {
return await ppixiv.extraImageData.getAllEditedImages();
}
};
/\x2f web/vview/sites/pixiv/site-pixiv.js
var allDataSources = {
Discovery: DataSource_Discovery,
SimilarIllusts: DataSource_SimilarIllusts,
DiscoverUsers: DataSource_DiscoverUsers,
Rankings: DataSource_Rankings,
Artist: DataSources_Artist,
Illust: DataSource_Illust,
MangaPages: DataSource_MangaPages,
Series: DataSource_MangaPages2,
Bookmarks,
BookmarksMerged,
NewPostsByEveryone: DataSource_NewPostsByEveryone,
NewPostsByFollowing: DataSource_NewPostsByFollowing,
SearchIllusts: DataSource_Search,
FollowedUsers: DataSource_Follows,
RelatedFavorites: DataSource_RelatedFavorites,
SearchUsers: DataSource_SearchUsers,
CompletedRequests: DataSources_CompletedRequests,
EditedImages: DataSources_EditedImages
};
var SitePixiv = class extends Site {
async init() {
helpers.html.setClass(document.body, "pixiv", true);
if (!this._loadGlobalInfoFromDocument(document)) {
if (!await this._loadGlobalDataAsync())
return;
}
let html = document.createElement("document");
if (!ppixiv.native) {
helpers.html.moveChildren(document.head, html);
helpers.html.moveChildren(document.body, html);
}
if (!ppixiv.native && ppixiv.pixivInfo?.pixivTests == null)
console.log("pixivTests not available");
helpers.html.setClass(document.body, "premium", ppixiv.pixivInfo.premium);
helpers.html.setClass(document.body, "hide-r18", !ppixiv.pixivInfo.include_r18);
helpers.html.setClass(document.body, "hide-r18g", !ppixiv.pixivInfo.include_r18g);
let preload = document.querySelector("#meta-preload-data");
if (preload != null) {
preload = JSON.parse(preload.getAttribute("content"));
for (let preloadUserId in preload.user)
ppixiv.userCache.addUserData(preload.user[preloadUserId]);
for (let preloadMediaId in preload.illust)
ppixiv.mediaCache.addPixivFullMediaInfo(preload.illust[preloadMediaId]);
}
return true;
}
_loadGlobalInfoFromDocument(doc) {
if (ppixiv.native) {
this._initGlobalData({
csrfToken: "no token",
userId: "no id",
premium: true,
mutes: [],
contentMode: 2
});
return true;
}
if (ppixiv.pixivInfo)
return true;
let pixivTests = null;
let pixivTestsElement = doc.querySelector("#meta-pixiv-tests");
if (pixivTestsElement)
pixivTests = JSON.parse(pixivTestsElement.getAttribute("content"));
if (ppixiv.mobile) {
let initConfig = document.querySelector("meta#init-config");
if (initConfig) {
let config = JSON.parse(initConfig.getAttribute("content"));
this._initGlobalData({
pixivTests,
csrfToken: config["pixiv.context.postKey"],
userId: config["pixiv.user.id"],
premium: config["pixiv.user.premium"] == "1",
mutes: null,
contentMode: config["pixiv.user.x_restrict"],
recaptchaKey: config["pixiv.context.recaptchaEnterpriseScoreSiteKey"],
hideAiWorks: false
});
return true;
}
}
let globalData = doc.querySelector("#meta-global-data");
if (globalData != null) {
globalData = JSON.parse(globalData.getAttribute("content"));
if (globalData == null || globalData.userData == null)
return false;
this._initGlobalData({
csrfToken: globalData.token,
userId: globalData.userData.id,
premium: globalData.userData.premium,
mutes: globalData.mute,
hideAiWorks: globalData.userData.hideAiWorks,
contentMode: globalData.userData.xRestrict,
pixivTests,
recaptchaKey: globalData?.miscData?.grecaptcha?.recaptchaEnterpriseScoreSiteKey
});
return true;
}
let nextData = doc.querySelector("script#__NEXT_DATA__");
if (nextData == null)
return false;
nextData = JSON.parse(nextData.innerText);
globalData = nextData.props.pageProps;
if (globalData == null || globalData.gaUserData == null || !globalData.gaUserData.login)
return false;
let state = JSON.parse(globalData.serverSerializedPreloadedState);
let mutes = [];
for (let { value } of state.mute.tags)
mutes.push({ type: 0, value });
for (let { value } of state.mute.userIds)
mutes.push({ type: 1, value });
this._initGlobalData({
csrfToken: state.api.token,
userId: state.userData.self.id,
premium: state.userData.self.premium,
mutes,
hideAiWorks: state.userData.self.hideAiWorks,
contentMode: state.userData.self.xRestrict,
pixivTests: state.test.ab,
recaptchaKey: state.misc.grecaptcha.recaptchaEnterpriseScoreSiteKey
});
return true;
}
async _loadGlobalDataAsync() {
console.assert(!ppixiv.native);
console.log("Reloading page to get init data");
let result = await helpers.pixivRequest.fetchDocument("/request");
console.log("Finished loading init data");
if (this._loadGlobalInfoFromDocument(result))
return true;
console.log("Couldn't find context data. Are we logged in?");
window.ppixivShowLoggedOut(true);
let disabledUrl = new URL(document.location);
if (disabledUrl.hash != "#no-ppixiv") {
disabledUrl.hash = "#no-ppixiv";
document.location = disabledUrl.toString();
document.location.reload();
}
return false;
}
_initGlobalData({
userId,
csrfToken,
premium,
mutes,
hideAiWorks = false,
contentMode,
pixivTests = {},
recaptchaKey = null
} = {}) {
if (mutes) {
let pixivMutedTags = [];
let pixivMutedUserIds = [];
for (let mute of mutes) {
if (mute.type == 0)
pixivMutedTags.push(mute.value);
else if (mute.type == 1)
pixivMutedUserIds.push(mute.value);
}
ppixiv.muting.setMutes({ pixivMutedTags, pixivMutedUserIds });
} else {
console.assert(ppixiv.mobile);
ppixiv.muting.loadCachedMutes();
ppixiv.muting.fetchMutes();
}
ppixiv.pixivInfo = {
userId,
include_r18: contentMode >= 1,
include_r18g: contentMode >= 2,
premium,
hideAiWorks,
pixivTests,
recaptchaKey
};
helpers.pixivRequest.setPixivRequestInfo({ csrfToken, userId });
}
async setInitialUrl() {
let args = helpers.args.location;
if (this.getDataSourceForUrl(ppixiv.plocation) == null)
args = new helpers.args("/ranking.php?mode=daily#ppixiv");
if (!helpers.args.isPPixivUrl(args.url))
args.hash = "#ppixiv";
helpers.navigate(args, { addToHistory: false, cause: "initial" });
}
getDataSourceForUrl(url) {
url = new URL(url);
url = helpers.pixiv.getUrlWithoutLanguage(url);
let args = new helpers.args(url);
args = new helpers.args(url);
let parts = url.pathname.split("/");
let firstPathSegment = parts[1];
if (firstPathSegment == "artworks") {
if (args.hash.get("manga"))
return allDataSources.MangaPages;
else
return allDataSources.Illust;
} else if (firstPathSegment == "user" && parts[3] == "series")
return allDataSources.Series;
else if (firstPathSegment == "users") {
let mode = helpers.strings.getPathPart(url, 2);
if (mode == "following")
return allDataSources.FollowedUsers;
if (mode != "bookmarks")
return allDataSources.Artist;
let userId = helpers.strings.getPathPart(url, 1);
if (userId == null)
userId = ppixiv.pixivInfo.userId;
let viewingOwnBookmarks = userId == ppixiv.pixivInfo.userId;
let bothPublicAndPrivate = viewingOwnBookmarks && args.hash.get("show-all") != "0";
return bothPublicAndPrivate ? allDataSources.BookmarksMerged : allDataSources.Bookmarks;
} else if (url.pathname == "/new_illust.php" || url.pathname == "/new_illust_r18.php")
return allDataSources.NewPostsByEveryone;
else if (url.pathname == "/bookmark_new_illust.php" || url.pathname == "/bookmark_new_illust_r18.php")
return allDataSources.NewPostsByFollowing;
else if (firstPathSegment == "tags")
return allDataSources.SearchIllusts;
else if (url.pathname == "/discovery")
return allDataSources.Discovery;
else if (url.pathname == "/discovery/users")
return allDataSources.DiscoverUsers;
else if (url.pathname == "/bookmark_detail.php") {
if (args.hash.get("recommendations"))
return allDataSources.SimilarIllusts;
else
return allDataSources.RelatedFavorites;
} else if (url.pathname == "/ranking.php")
return allDataSources.Rankings;
else if (url.pathname == "/search_user.php")
return allDataSources.SearchUsers;
else if (url.pathname.startsWith("/request/complete"))
return allDataSources.CompletedRequests;
else if (firstPathSegment == "") {
if (args.hashPath == "/edits")
return allDataSources.EditedImages;
else
return null;
} else
return null;
}
};
/\x2f web/resources/noise.png
var noise_exports = {};
__export(noise_exports, {
default: () => noise_default
});
var noise_default = __toBinary("iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAMAAAD04JH5AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA9QTFRFIiIiISEhIyMjHx8fICAgSEHIqwAAC3FJREFUeNrEWwmS4zgM46H/v3lFALKdtA9ZqdrZ2eruOLYOigTBw2bNI8zMPSyj/+gfmmf/4Ob9evRPFvUz+ufsV3FTfVV/pXm/1CJwc/\x2fUf7V6pH7171tmv1i39a8yMVMN5uE1Pp7jF+59lOxP1MD9oYZF1dVW9wTurgvh2Vq/ln2KrK/rEQ2Me2p5juVn7UibqhtqkX1Ex68aAHtyzI4d9IVm7dxwvdaZfbC61P/Dx5qrj9Oidmu1in5nTeh9MRinFuv4xrDzaBQXpUmhjY2XxEq0/ck+ZotaRq2iL7rVOI0rq8my1YZbiRXDY5Kxijqd/kU6lu0YsGRUR4pJa/utpg5+dNyF1eDEuBivxTbtwrD5Einur7XWRHWxRsUTXXQ4r1IhCHZoAf7Qpg0D1x01UHA7UVfquZqjBaXKhfUffbONmolN9mkSCseF1+JKlRqVFDqBLZY61OJrV9gM5tOMdeyRLl0ZKoARjbpGlcWaalgM3+XeSqz4QY0MN4kREtGia9pmQ6JUuuRMVMw6lJCosWytpizEptW9SbA1IY6qnky7VffQXmvU3M2tydyitMKgcFmbd9hTUkFkkbiQPBx97kJodXDQX2mTUdW7DDMJK0Zb6daB8a32khST0X5SgqoVZP0ri6s11sKjbKJugUJCIJAu9Le23uoyBInvqNMewKnSgEbF0k8YpBGfiD51KoVwFFYJiDYfsHA+BN0pOQSUiVrNfVIJeBXajbOrcQsyaOWtbJGy9PqrCfwa4LKG7EKAygk76mMQZaIEkHi6JOhcZ0qFMbfzWAoxpu0Fplb3AtJKF1LncWrgCVk6BqSKDHG3zZoi4tTAC7biGmBcAKMjTYEz7Y8DQlus0BEYiVtLU7k2T6Injo9X5M/6uSYtEHae2iDRSUdNdYLZlIUBO2pi7pBgj2NygHN9gh3VzriJ2h1NBCdj0GfKnstqtDkidHnCOqc6VKgAdI2QTPMpmUPRqUZd9yCvhEXKkulPAqBS5+H8sgZKumPYEUEJqNPtsQZNInUOJUqTvuIeaiFNBMCBk2lE1+HGahfcUw0tadKTBCSZmx7D7HFctSg5f4nS4cxyc42FqWMGcAo4RsBuf6alnN4zytTTKXQBrkH83LI8s8GWB8JBQrVoK3SCspb+l7G4VI2jAWhr81yg88BLTIBqI6GwRyWhQ2o4ZqMsiONh44RfK95xzAXFkwU0I5isuMADC5p2gSEX6JnaOFE51smQbzt+SYbokxpvt3vKZzJWOk1qQyN/hBKcUT7fKR8M8ZJykhOSDjl8xvCOL7bVSMbqofKlMPLi6hCZwatA5WgUIXdA/5Yh39CVphWI4hTcpRF0qU98JykPkXHAPOAJHLl0ETSDdE80eVALKOdmrHc0+k6mvygwlphECXoxuOPSrBoXgjFAM+iLK1xJMekMEZtB6hRqkHEkhsEJiJg0Wm9NnIiCAEGUOmMb6DyW+Mq9FMErxQzqZStgLFVrSWdefGxz46UxJI7SpDqA3TAXuPGsrhyBwQQMACI/kosRTAChFYnW6gJPJglVEMJk1lPRyxe5GVzSBD+FMqkztWFHODXLLfZJ+eVWaJVGqic8u2BrPqh1Uj2CTjxpOLBJOM/3tDoOtPrC4fnB4fmNw2XcNEM+r7ZzRz5d5BOzH8iv7+TXOd4pXLQdLvwarhihQ8VJMMWcUsSSxPwcrhoW9kGWXGTJ5CCeyZo12jx2MnyI4oonsubiRjDl1UxNY8blVaZGCZUQr35i8feG9hEmTaYJ5OgLeZv4zQIAQK7NXwGAuH8IFsW6llIrg034nduNEVbCC/x1+zbI57sc12fQ37bTg/qD0kCfEiBjigxIZ6BIJhNJ41cF9jR3xtYMXhtDpood7jV9hVKHjRzdWPZ8GPAHFSfCgD+oGELFICMZ7A4ZAaRGsIwGo0Mupt8YDFv/et7phOKBXTaxy7K3c8V6QZNX+GTsig2HPuLEJsK0OdqRoUlkPhBI0emHvCSkMZXLajbCkgJ2HJkwN45+0nc/WUYy46eXsiq7YwNFu4qgbCp1QM/XfJHb0Vt8pz2SaUyJ2CjYGKYw9sS1n6c9btMutqddQCiecEPpBcRBf3LzoNLURMJ0yQqL46qc1gONhv2G/L7cOaznK5H5qm5ws7ZHTJW6vix3xF7uALSNVA3oS5JkUlfluuFPcHIZYi2xOfEG5Xo653sZv0+vpfRMudSLsHcSGFcrLTsBgD/zCpaSCZE2IN1HdjbodZzRBSkAoK38SxpTV4J4rnaEktJmMEbaWMMtUPkgOf3fDwDBDAw9XCrxjkp+5v2+clZwjbMOfk83vxA5U4iDD0bMiDwlcmpWY4GgwduDTE3x/6sQdIb/f6crlOp0AvV39vslP50urx34ae78dLCfp/qsjWwhnTNJtsONTwLWMeowRR3yWd+JN6UPGKsVMoJVbQWmZJIkVAhZYTQmRtOo0NJ+2Cc535uA7zZ2DxF3RVooICRWioQ5uJUvINgxceg/JLnaoOe/VO5WyLTtKQZBF/UwRJIY9Eajx4T5cKlJ5QlW8lS5JVwRFRtS9wY5ZI48ECsCm2i2FDllmGd3hO5g9Hq7wq2AmFtKCiIhaXSW0i5WCL7DlLuKivIBoCTtkK+4qYmsMOl+fiPcTAaQRp4bPODBIyB9uCj+7VuMZWKmIwBdL/vYPH5e5G8wTUt/Y86K5IpO5JoMUzKE7CajEVM0gv8ZDcH4c8n6d/60kuLOnT9hS/cFnofw5JD6nQ5PQuEJnaC4+sscwxYrrNAoU6wQAqHpvN6ZXn7BvHB/Wi8JNK/D26NC3bRqXEU4sUc4qTreevn2Gar+lm9t8BBLFjjOG2x8qsHnO2tXYMvWi+sGH1eDjyvJ/G+zZH5Odp2w81yYPpDd6fgmRbYbe0p+coa24gxN7lpK/IsR0hBWjZBSPjBcHrkahtJEdEP82Qf+i0iHVOdtB6SGNHJW1Tb+9gfN9sdd9Adl3PQnmfqT4M9zoUryESW9rJKwYLMdZGMT0ElvyDxJ2XtDQr0hTyTFBG7ixctlY/rHhbKxq/uTe1WD5yozXMmxtp3V+HNh8ClqeCgMnkUNOTp5MNj7aOxDsSajsQ+EiCNC2E2X10yXmfRODXxAYkHndZeZH7rMlGE/Ted5TNTk3nZ+4RiVThzdjFMteFdJiBUM870F0JnBuQeu28bKkRB+01hpAk6gx11FbqZwclGRuy2cuOp1Q76vz/2YlZk996s0MpX+h/hwBnq/eaXt0O/DOT40jl6nzlZKzq7UGZpMXrQB0+N8tSF/9zmrRBlUo2DfjMswtD3xXQe2+utK4WePzm99aNzfY8fcXeJutsZ9ZlGIOeO3gkksFEy2rgcI0yezm1fcbwm/tTusP992MH6dx0rPgek8mnT9b5/Ni3LoyksZivBteF+Y3fayAASIw5vqknubWw7RR7VOsIzw1AF534rB8nNjYlTpkpbi7YFivSPmCOrf3oFJRHupwX90bLbh6OArmnzFaHD9iZaulP5zT1gyS/aTGLeUpTX1WqKGlGFbI/GoMquPmYV33wLopfzkHjktlBtMBJfSnn5D7DxGWXk9yfYYxfSu1HLFJlaoVBPQ2Whnm8hJXx7wi1cRNs7jO+dpQ7DLOfuV7hdTZDYq5L90Fdv7ruLqpOCJjszty+TsZ7PEZI37o5HDlVqzrQ61UnvThfO+2ft+ICXqYrwxNv9q3Um+9vHVupN8bagjhWfwz3vJXhfdvrOIM+/kfWYRc88ixu07oVPx6tQ7eZ/xqo6X7MhftGSfqtZ9D/Mpt8qdW9H+n7kLI8oz7rRCJ00oHwSn/52PROyxK0O3y/chZ9IGtcoWzHwyX6hA7DptYEobqCx2ipa+o+X9OyAPJ3TKbkNozXfOliMCOuqXXcgfjppt5f+4m+4/AQYA7rFTSAXDOQYAAAAASUVORK5CYII=");
/\x2f web/resources/apple-touch-icon.png
var apple_touch_icon_exports = {};
__export(apple_touch_icon_exports, {
default: () => apple_touch_icon_default
});
var apple_touch_icon_default = __toBinary("iVBORw0KGgoAAAANSUhEUgAAALQAAAC0CAIAAACyr5FlAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFDWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDIgNzkuNzhiNzYzOCwgMjAyNS8wMi8xMS0xOToxMDowOCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIiB4bWxuczpwaG90b3Nob3A9Imh0dHA6Ly9ucy5hZG9iZS5jb20vcGhvdG9zaG9wLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB0aWZmOk9yaWVudGF0aW9uPSIxIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyNS0wNS0xOVQxNDozMjoyNiswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjUtMDUtMTlUMTQ6NTE6MDQrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjUtMDUtMTlUMTQ6NTE6MDQrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjk4NTI0ZGE0LWMwZjgtYWM0Zi1iY2NjLTNhODRiNTAyMjhmZiIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDo5ODUyNGRhNC1jMGY4LWFjNGYtYmNjYy0zYTg0YjUwMjI4ZmYiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo5ODUyNGRhNC1jMGY4LWFjNGYtYmNjYy0zYTg0YjUwMjI4ZmYiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo5ODUyNGRhNC1jMGY4LWFjNGYtYmNjYy0zYTg0YjUwMjI4ZmYiIHN0RXZ0OndoZW49IjIwMjUtMDUtMTlUMTQ6NTE6MDQrMDg6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyNi41IChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6S8wm4AAAQfElEQVR4nO2de3AV133Hv+fsXt2X0BMJCSQhyYAREmACJGC7sYHaodgOjhucic0koY+kSTrT1EzSdtzU00nGCZ16kpk4xpNQkmJPk2Bq0g4JxuA4pgw4PC0QoEjo/TR6gnSle3Xvnl/\x2fEBZC966QdPfurvaez3+2xO5qz2fP+e45u79l2D0CiSQW3OoDkNgXKYdEFymHRBcph0QXKYdEFymHRBcph0QXKYdEFymHRBcph0QXKYdEFymHRBcph0QXKYdEFymHRBcph0QXKYdEFymHRBcph0QXKYdEFymHRBcph0QXKYdEFymHRBcph0QXKYdEFymHRBfVsj2TZXt2AsyMncieQ6KLlEOii5RDoot1mWM6sHGDLJkVV8Z2atoe7YZN5Sh0Y/u9/AvlvHgOcyu3zRiO4NoN2lslXrkqRgxqMRdDgRvlGezRhXyuB5kp7P4i5lXhGterhgWGIzjZTH0j1B3E203icj+1hhB2tDXMsso++qeVA7/+M+WJYt0hry+EH50XL5zXZjgqEtwcn8xhn1rIS1OxqZT7XVCmmf81QiCMd+pF/SCONInjXRQSJt1EACbtyI5y5LpRu92VljLZv+4O4kuHI7/poGmcJoKb46Ec9ngJ/+xSnuudthB6aITrwzhQLQ41iPfMsSRp5VjoQ+MXXXfdQFUvfWJ/ZGgqHTthqR87limfL2P5PqYmLIVrhI4A/fIq7b2iXQ0ksgmTVo5iHxqmIIdGeOmM+Iczkw4uhGV+fKVC+fwyPtdrUq9PQNcwfnFF/PSydnkgMXeEySPHhPh5fZDmpU7pr+8O4k/fCFcOxDpZhDI/\x2fqZCeaacZ3vMCwPj9o/eIPZfFbsvaZcMVyRJ5IiOnx9OWQ4AZ7to7YHIhC17Ob69StmxnM/zWaDFBLqDeK1KfPusFjAwiyTJ9PlcNx6aP/PDWJnNvrdagfjovwUey2enn1a/uZbn2cAMAHM9+Ps1/PTn1Cfy2e3jnA1YL4dXweQ3JpPj4vjrFXxdBoOAn2HXWmXfFrUiK4Gpc2Ysy2Q/36J+b61i9YFMA+tPYfwXd7YH/\x2foJ5bEcdvEZdedanuU24KgSQZYbO9fwE59Ry3yzY87VgsxR6Mb2JZPFz2lljlkHAX/sp52/037bOZ1Jmgk4MnNw4McblRcfUJZmMI9ii0xgMgxYmsFefVR5aoHdI4jZcsx146F868cyyylMZXs2q99do9jZD7PbKc746SQy3fjGx/iuj9vXD3kRW4nfha/fx7+z2qZ+SDksxu/C11bxzyxgNrx/kXJYT5YbL21UNufZzg8phy0oSWMvbVDK/FYfx51IOexCWSb76adUj51u7qUcdoEBH89lL3zMRuFUymEjXBxfXsk/bZtwKuWwF1lu7HpYWWKP8GHTp8+t4konuVW0D1CGhykK8uYwAJ0DpGnoD1L+HBaKoDwvsblgcTr7eoXyd+/P9PFp45ByAEB9DwVGwFNQmMU8CoqyGGfg7NbST6aXCYIgJgg3QjjZTi4NGR4szkmIJQrD9gp+qFEcvW7x6JLscpxoEBk+ljuHlWTrrgIyQGG3HlXP9SHXx8ICrQN0qoXy/CjJMl6RLDe2LeJHr2uGb3laWN1zWUdTH73TTMvyecU8ljvNZ8ZcHCXpbF0hU1042Szqeoy/xJ8u44/kWnxfm6RyHG8UiopNRSzLM/ONMKBwDltfxG9qON1isB/pKdi2yOLWSbphpa6HagLYXMyZQfGTAatyWV8QhxvFklR2z1zDLveny/j+WnGsy7LkkVxy1PdQUwD3+lHVQcbGz0wPNhfzd9uI9VBptjF+pKfgqXv4seuaVc9EJdGwUt9Dh+pErhupXlaRz8qzWVoKUhS4OBR2+/yPxk8Xh1tBrg/3z2f3LWBqCk61UEPvZBcxAzYuYE1DqDcugvz5Ul7sNWpj0yZZ5KjqpIONtH2Fkuj4uaGQNQZgVETN9mBFpmWxNCnkONNCl27SztXcnPi5sYg1Dc18R+NRGB7TrzaQaJyfOQbC0IAtxfxgtWgdwu9bRFcI9YN0IwIA6SpKU1mOGw8X8gI/HinlPhVc/1qdYvzcUMiON4pPGtGuT97LX7ygNQXj39K0cbgcEYE957T9jXSun2IWWhnU0BYiAG+2ayC439U25LJPl/BtZZO9YXvX+MmA0mzW1EcL4x4Usj1YnsGaOi24Z3H4sPKLy+K58+L9vthmTIQhRHjrQ/raKe3+X4Z3nRYdQ7rro3eNnwVzWG1gxgd+G4Vhs0Uji5PluDmC12vETO4DGWqH8U9ntYf3h394VvSGdH9x8vi5qYCdbzPgit+6mLmsSKVOluNAtXg7nrUrhpphPPcHbftvIpU9pOlsaZL4yQBPHBF4jHk+VmDFO56OlUMjHKwz4pkqjsOdtP6NyMvnRVBnIWw0fsb8UUk6a5x0dmQquDjKMyzoOhwrR3cQH/QZFOIYhgnfOK396LwYjsT8+a34Gf0jj4oOI5LHxkILWsqxchz8o2g1+vbvW6e1H1+I7Yde/GRAdhoux32vUZQa5wZmgmPlONw4oyg6ORzfPK3t/kCEY40hevGz0M88d69wdhc2lciewzhahxOzXY7nz2qnYvUEevHTraAn7gnTVJcFq2/OlCMi0BhI1KxRkPCVo5H2WNuPGT8Zgzfud8cVDo/pbeVMOYY19IcTuP3qAF6tFKGom5eY8ZMBc+NODKNrxSbjTDlONVFi3wxi+E6lOBf1GI5e/JzrNaBhpRzGMKw3Y2UoL5zQIlEOxoyfLiNOs8/0dTBnymEO73VT8+BEC2PGTwIq2+3xFtt0cKYcHlO64DDh9Ss0oc1jxk9NYOX8eA9pKNb8SkJxphzri0wan1+v1oJ3tlnM+BkwIh2bMlTegTPl8KkmPahSO4y3o1ZwouPnYCjehiUph1GoHPlmLWPWD0z8PxPiJwFDcdd61QSCppdmcKYcACrMWsY82jyx0SbET0HojntGzvzAAQfL8UiRSX9aVf/Ehp8QP4cjyPHHa+rJ5onJ1wQcK0fpHJN21B6a+CjhhPjZMkCL4n4NbsCKTw06Vo5H7+GLTXkdSAO0OweW8fGTgIj+U4ZTp8Ogdx2mhWPl8Kr4UpliTv2kkXFyTIifLQPkj3vVTRDearKgUphj5QDwxQrmM+XvO9caO34S0NGP+F+dHYrgUlSyMQEnyzHfz/55lRmdx+qC2PGzdYByjajudaxetBoxNk0XJ8vBgB3L+bIEF1/jQMq4szgWPwlo6SND6v5ET6WYg5PlADDPh79dkdjKnhkqlI/O4vj4eeY6zfMZYIZmUeCA4+VgwLPliS07vzCVjSkwFj9r+ygUIEMKuVwfxnGL6rc4XA4AaSn4tw1KeWqiPqtW8NFzo2Pxs2sYNwL4E4MeCT5QLUIW1TR2vhwAFqWzvZvVUl9CNj72Iuto/CSgtkusKTBm8j4icKghAY/RT42kkAPA6hz2rfuUYiNeThyPi+HJJbfjZ3EWO9tG9xs3c98aoLje6IyPZJFDYfjLFXzfZnWJ18jxZV0Wy/UyAGeuk8LZyWaxdoFhlzkBr10mC+ugJ4scAFSOB/PZwa3qljyDPstI2FrKVY6aPqpqEyToAUNX+9oD9J/VllWLQ1LJAYAByzLZvi3q99cqXhZvF7LIh2eX8epeXGihh4v4OkPNIGBfFdVZsaQyRnLJMUq2BzvX8FPb1Mfz47gqCc8uUZoH6FKb9rkV3KjykmO0DdJ/XLWy20ByygFA5ViZzfY/oZ75rLo1nxW6p9mLENalY30OyjPZtuXGf54+LLDnkrC224Dja4JNjlfFmhz231vV7iDerBaHGsSlfuoIQe+pKxXId2N5Bnu8hH+hgvsS9v5qVS/tqrTsDnaMpJZjFIVhnhdfXcW/uoprhKEITjVRSJAY15dwBjdn6xcyn5rwN896Q3j+uBYUJn2wfhKkHHegMMxx4dFFzKqW0Qh7L4rDHWSHAd8GhyAZx+Ve2l1l/TeaRrHHUUgAAN1B/ON7Wn2CKotMHymHXRiKYPcH4nAHWR41xpBy2IKwwE8qxb9csMuAMoqdjiVZ0QjH2+kHlRZ/0S0aKYfFaIT/66C/OhpptuIp0cmRcliJIJzooL84Emm04qMId0XKYRlEON5OO45EGmxpBqQcViEIv2ujHW/b1wzIGVJL0Ah7PhAvXtBsmDPGI+Uwm7DAy+fEc2dtd28SjZTDVAIRfPm3kf9qscXSyV2RcphHXwiPHQifujFrkp6UwyROtdJTRyKdI9YvxE8dZ8pR2U7nO+n+AnZvrvVNUddNv6oWr1SLzkRW3E4EzpRD4XighLUPY6CV0txYMumXxhNHTRf1B/FGnfj3qxa9sxYfzpSDAUsy2ZJMDEVwpY/Ot1GaG/HXXpo617rpZggBBS+f1/a3zo74GY0z5RjDp2JNDgsLtA7Q+y2U50exETURJqGxlzoDmJeGAj978s3ZFD+jcbgco4x+jL44HTdDONNJLIJMDwx5BX6Uy53kdaFvGEEVFZmsKAt/aKWnjoRnV/yMJinkGIUB6W6szWOCMBTB1T4KDCESoZxUFgpjWd70mvFKJ3lc6BmCNwXpqcj1suIscIaaLvrBVbGnZvbFz2iSSI4xOEOqC2WZTGRAEButHN0RoIFhaAKBEQRGqCCNjS9q7lHRepP8KcyfAoVjjhelOczFUZwFxm71DjVd1BnAr+rEKzWzMn5G40w5wlObm+YMnAGAC8j3szw/BEEQiGL0IoWZjLFb/2TCj691040g6oK0t1Ic6Zyt8TMaZ8oBhhnEz9FPZU3rtZTR+JnuR5fA8ye0a0OzOH5G40w5XBwr5zMT4md5JlNS6CcXxXcrBTC742c0zpQDpsTP3hD2XhKvVmm1Q07TYhTHyjFGIuLnjRHsqxL7a8WxDwncmWYgGeQYI/74SUBPEPuvijfrxDujFf4clDCicawcdLfreVrxMyLQGqDXLtO+au2aQweRaOwoR/xnXhBGNLjjK5wxGlaO1Yv6ARxuFMe66JZxyWEG7ClHStxFDn7dRD97K1KewTYW8oWp2FTKU11T6iQ0wmAY79SLxkEcbRZV/XRH0fGk0WIUW8rhQvtNmp82w6Z4/0P63wbREERDJx3q1AAo72q5KSj0s3QXMlx4cAFfOO4Djk2DONEm+sO4EUZLgK6PYBY83mkKdpTDp6ILqGwnr2tKc52BEXQMkwBGNHQF8cOLWt2dsUADOkbQMXKrGssbbbL1p4Qd5QCwMI0VpU11Fvx/asTTv7/zV5Os/08Q1suh144MSJlaohxxyDqX7bD+Pj3++GnV50gcjw3kcKH95syrxY7GTwOPRzKG9cOK4fFTYhRmy0EUY+5Sxk97YrocQDACb9RuZfy0IWZnjqBA7Y246tHL+GkaZsvRFcKrF0Xz4B31gaeOjJ9mwrB75O6/ZSgq8OBc9sxinjvl7ybJ+DkRU86ABXLcwroPEDkBU+Swfp5DYlukHBJdpBwSXaQcEl2kHBJdpBwSXaQcEl2kHBJdrFuyl7Octkf2HBJdpBwSXaQcEl2kHBJdpBwSXaQcEl2kHBJdpBwSXaQcEl2kHBJdpBwSXaQcEl2kHBJdpBwSXaQcEl2kHBJdpBwSXaQcEl2kHBJdpBwSXaQcEl2kHBJdpBwSXf4fdgpWO/WGtV8AAAAASUVORK5CYII=");
/\x2f web/resources/favorited-icon.png
var favorited_icon_exports = {};
__export(favorited_icon_exports, {
default: () => favorited_icon_default
});
var favorited_icon_default = __toBinary("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAGbWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNi0yMFQwMDo1NjoyOS0wNTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTgtMDYtMjBUMDE6MjM6NTktMDU6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTgtMDYtMjBUMDE6MjM6NTktMDU6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTFkYzgwNjgtMDM1Ny0xNzRlLTlmMDAtNThjYjk4NTQ4OWQ2IiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6NzdlYmJlZGEtYjQ4Yy0yYzRkLTk2MTQtYmM3NmZmN2VjYTU5IiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZDFkMGQ2NzQtMTBkNC00MDQ1LTliZGQtNTY2ZDYxZTNlYTU0Ij4gPHBob3Rvc2hvcDpUZXh0TGF5ZXJzPiA8cmRmOkJhZz4gPHJkZjpsaSBwaG90b3Nob3A6TGF5ZXJOYW1lPSLimIUiIHBob3Rvc2hvcDpMYXllclRleHQ9IuKYhSIvPiA8L3JkZjpCYWc+IDwvcGhvdG9zaG9wOlRleHRMYXllcnM+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ZDFkMGQ2NzQtMTBkNC00MDQ1LTliZGQtNTY2ZDYxZTNlYTU0IiBzdEV2dDp3aGVuPSIyMDE4LTA2LTIwVDAwOjU2OjI5LTA1OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDoxMWRjODA2OC0wMzU3LTE3NGUtOWYwMC01OGNiOTg1NDg5ZDYiIHN0RXZ0OndoZW49IjIwMTgtMDYtMjBUMDE6MjM6NTktMDU6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+LpZUDAAABwxJREFUeJzlm29sG2cdxz93tnOOHedP4yZuq7QpibsSBeKWsVWZyqAQaWKTJmBVEjQoSBOVKAiYqkkVlYbGEO94AWKiiGkvOmnrRMUfsSpNUatVtEQg1JCkLWFZk5JiO2qSJk5jJznfHS98h52ssc/2+W6wr2RZ57t7nu/zfb6/3/NPFjj+FhvgA14GngMCG2/+jyIF/B74NnA394a44cGjwCLwPf5/Gg9QDfQCM8DPAcG4kSvAb4FfAG47mdkMAfgm8A/0thsCHAWedoiUE9gD/A4yAviBVxyl4wyeAlpF4Je8Pxd8WPCmCHzRaRYOYr8IeJ1m4SA8jmR8yS1T5UojudOspt2sKW5W0x4nqDgz5LXUz9HeGKc9GGdiNsTEXIiJ2ZATVOwXwONSONQ+xtEDF9i/Y4q/R3dyaqiH6YVGR1xge/YP+hN8q3uA/TsmARdd22/znYPn2OpP2E0FcECAjuY7fGzbv8jORgUe2hqlMzRtNxXAZgE8LoWe8Ih+Jaz7/mx4FI9LsZMOYLMAQX+CIw+/s6HqzPdXP3GZoANhYKsAHc13CAUWyFmM6RBoqlmko/mOnXQAGwV4sP1Zd90THrE9DGwT4MH2X0/jyMPv2B4Gtgmwuf0NCIQCC7aHgS0C5Lc/6363OwxsEaDeu0z/visFqnQmDGwRYM/WGDvrZ9m89w3YHwYVF8AlqhzcfVO/KiwAwKG2MVyiWlFeBiouQJ03SW/kqn5VSIAMncNdQ9R5kxXlZaDgatDjUvBXrdDou0+tN0m1Z61gM3IR2T5FZPuUfmXuzXAwRn/kCsPRVtP1aEBKriKx4mMuWcPymhdZcRV8T+D4W1q+BwJSisj2KXojV3nukYtIbtk0qfUQMWc4Vf8Uj9W0h1/95RBnhrsZjraytFptilVeeN0ynaFpjnUPIrnTZHqx1I8ZlF6+5E5zrHuQztA0XpMdVTAENARqpBUyJnMV0ZBSIej1lAINUKiRVtBM8izogJRcxWhsJ/GlekChVHtWHiqgEF+qZzS2k5RcZeqtggKspD2MxVt46cIz6yr6YCHbMS9deIaxeAsrJrfXCiZBAwEpxSdb3uNHT7zBgV3v6r/aERL5kLE8wNDtMN8f6Oev022mkp8BF92Hf2DmwTXFQzTRwOVbHSRlicdax3UC4IwI2dHiJ5ef4sXBXkZiu0jKUlGlmHaAAVHQaPQt0bNnhNd6X6HKZYwMpSauUqAAGpomcOTMMQb/2cXd+7WoWvEdYdoBBjQEkrLErflmzo9H2FE3T3swTqY3ihnuSkHW8ufHu/jamWNceq+Teym/6ay/EUULYEBW3MSXGrh6+yHmkgE+03ZdJ6hRmRl2NtH98I9f4seXvsCNGfPJbjMUHQIbIQoaW3z3+XTbdX769Gtsq72n37EqQWpkGq6xuOLj6K+/wcWJTuaSgZIsvxFlnwypmsDscoBzN/cxOd/E6/0/Y2/Tv8n0mBUHTxnLT8430ff6dxmLtxSd6PLBMq8mZYmxeAvnx7ssLjpTzvnxLssbny3dIvirVjn4EbNrf7MwDk7GqKu2folsqQDtjXH9zA+sFUAgHIxVZKfIMgFcosrjbTesLlZH5TZMLWNa503SFzE2Pq2eC1Ruw9QyASpj/1xUZsPUEgFKs78KpDG/vK5MGFgiQHH2N6azRsON5XWh+VhlwsASAczb32i8xsWJTo7/4StcnOhc93t+WB8GZU/VzNs/28CTA328MfwY8UQ9vxl7hP7IFV5+4k39mXwrSwHQ6AmPcPlWh6ld30Io2wGF7Z/t3dnlAIdPP8+poR6m5ptIyhJT802cGurh8OnnmV0OkN8N1odB2QLkt3+2MWdHH+XJV09w7uY+ZpezC5nctcSTr57g7Oij5BfB2jAoS4D89lcwFjInB/p44e1nuRbdvelcPilLXIvu5oW3n+XkQN/7ysjC2tGgLAECUorPhUf1K6P3N7d8oZiVFZeJkMhQ/vzea2zx3S+Hfk5pJaLOm9RPfcFIUIUsXwj5Q8IYOgV2Ndwl6Cs/D5QlgNctMznfRKbxxVn+AUjr3yqg5obEiXNfzrmVGSkm5kL4qtbKoQ+UsSUGUO1ZQ9FEtviWcQkqF979OC8O9nJ29ACxRAOKWpS+xsP/3VhUNZHFFT/XZ1q4MrUXl6gS9Ce4vdDE6b99iuFoK/PJmlLp65WVsSUWkFI0BxZprllE0URmluqYWaqzfNMCMm7bVnuPbbX38LplYokGYkv1LKT8ZZVblgCioCEIGqKgoWoCmiZYsk9nZ31lzQRVTQBNsO2grBL1iXzwDvrshCICf3aahYOYFMn8o/LDiq+LQBQ44zQTB3AL+JMx9vYDzvxjwRksAx+F7ORDA1qBSw4RshMTQCOwBuunwipwCOhwgFSloQA3gMeBMLBq3PgPsH6q+iD6RQEAAAAASUVORK5CYII=");
/\x2f web/resources/heart-icon-private.svg
var heart_icon_private_default = '<svg viewBox="0 0 24 24" fill="none" xmlns="http:/\x2fwww.w3.org/2000/svg">\\n <g id="SVGRepo_bgCarrier" stroke-width="0"></g>\\n <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>\\n <g id="SVGRepo_iconCarrier">\\n <path fill-rule="evenodd" clip-rule="evenodd" d="M4.6138 8.54479L4.1875 10.25H2C1.58579 10.25 1.25 10.5858 1.25 11C1.25 11.4142 1.58579 11.75 2 11.75H22C22.4142 11.75 22.75 11.4142 22.75 11C22.75 10.5858 22.4142 10.25 22 10.25H19.8125L19.3862 8.54479C18.8405 6.36211 18.5677 5.27077 17.7539 4.63538C16.9401 4 15.8152 4 13.5653 4H10.4347C8.1848 4 7.05988 4 6.24609 4.63538C5.43231 5.27077 5.15947 6.36211 4.6138 8.54479ZM6.5 21C8.12316 21 9.48826 19.8951 9.88417 18.3963L10.9938 17.8415C11.6272 17.5248 12.3728 17.5248 13.0062 17.8415L14.1158 18.3963C14.5117 19.8951 15.8768 21 17.5 21C19.433 21 21 19.433 21 17.5C21 15.567 19.433 14 17.5 14C15.8399 14 14.4498 15.1558 14.0903 16.7065L13.6771 16.4999C12.6213 15.972 11.3787 15.972 10.3229 16.4999L9.90967 16.7065C9.55023 15.1558 8.16009 14 6.5 14C4.567 14 3 15.567 3 17.5C3 19.433 4.567 21 6.5 21Z" fill="#ffffff" style="--darkreader-inline-fill: var(--darkreader-background-ffffff, #181a1b);" data-darkreader-inline-fill=""></path>\\n </g>\\n</svg>';
/\x2f web/resources/exit-icon.svg
var exit_icon_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg" viewBox="0 0 24 24" width="18px" height="18px"\\n fill="currentColor"\\n>\\n <path d="M0 0h24v24H0z" fill="none"/>\\n <path\\n class="arrow"\\n d="M10.09 15.59L11.5 17l5-5-5-5-1.41 1.41L12.67 11H3v2h9.67l-2.58 2.59z"\\n fill="currentColor"\\n />\\n <path\\n d="M19 3H5c-1.11 0-2 .9-2 2v4h2V5h14v14H5v-4H3v4c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"\\n fill="currentColor"\\n />\\n</svg>\\n';
/\x2f web/resources/activate-icon.png
var activate_icon_exports = {};
__export(activate_icon_exports, {
default: () => activate_icon_default
});
var activate_icon_default = __toBinary("iVBORw0KGgoAAAANSUhEUgAAAC4AAAAsCAYAAAAacYo8AAAACXBIWXMAAC4jAAAuIwF4pT92AAAGU2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNi0yN1QwMjoyMjoyOS0wNTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTgtMDYtMjdUMDI6MjY6MjAtMDU6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMTgtMDYtMjdUMDI6MjY6MjAtMDU6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MWU4MmI3MjgtOTVjNi1mNzQyLWJjOWQtMjIwMTM5NzJkNDBlIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6N2ZkYzUwY2ItYjgzMy1hNzQzLTllMjYtNzQ1NmM4NDFlNjM0IiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MzMyMzRmNjktNjk2OS1jNjQ1LWI0MjgtYmM1NDUwYTM3NDAzIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDozMzIzNGY2OS02OTY5LWM2NDUtYjQyOC1iYzU0NTBhMzc0MDMiIHN0RXZ0OndoZW49IjIwMTgtMDYtMjdUMDI6MjI6MjktMDU6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNvbnZlcnRlZCIgc3RFdnQ6cGFyYW1ldGVycz0iZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3BuZyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6MWU4MmI3MjgtOTVjNi1mNzQyLWJjOWQtMjIwMTM5NzJkNDBlIiBzdEV2dDp3aGVuPSIyMDE4LTA2LTI3VDAyOjI2OjIwLTA1OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiBzdEV2dDpjaGFuZ2VkPSIvIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PmQ/KUAAAAQhSURBVFiF7ZlNTxtHGMd/s/aaGGqDHRRLwQergko+lJhW4pJDqdQLpzSfoOHqU/sJGj5By8VXkk8QcuqlUtxjckBWLxxaqVYFVQnCdWiDYy/e6WG8Zr07a68X8xIpfwmtZp6Z2d8+fuaZF4SUkvdRxnUDRNV7Cx4HEEJMbsRKpwTMAc7TUb3316ScqEUd3gltIaW8GHil8zXwBbCGgg2rKvAc2KGcqIftdDHwSmcN+AZ4NF7HQO0AW5QT1VENo4FXOo+A74GCzizaDWg3EN1TOGsNGuNJZOI2cioL8eSwD9ignGhOBnwIsGgdIt4dIlqvwbaGj+O8fCqLTC+pj/Cr2YPfiQ6uJtoPqPj1ARtv9vyeHUMymcPOfAqGqTNvUE488fUZCV7pPEZ52SfRbmAcvYwMPKB4Evv2Z0gzrbP64IPBK5054BkaLzsKAy6TOWR60Qek7WuYdO/cD4r9FXf6dMDjA01UaLxgMP82PeXhwDML2JnlfvnjuzESpqBjSQ6ObNpk6ebXVagd76pGtoVxvIudu68bchtY8Vaer5xqAnqha8BGWOhufh07s0wmZbBaNFktmszPGqSnBfOzBvcW46wWVTzLZA45s9DvK6wTxH913bClHpsGXBm2PdA7lBMrKI8PB777Fd38er+8lI8Nbe/A25nlAS8bJ78HdfHNNcfj2576GiE93c2vg2EiWodhmvf1+Sc9z7vngG0h3h7omhd6K3RfDvimq64JPBy2CPjUi9FxFHP9KO45Id4FOuCBu+CA/4jaAAFUx9k7GEcvEda/YZtrNRDr7UZQs9LAewF63nVCY26cl040p4NaffUrcMldOM8qaoOzCTydHEVIeVbfML/gYB4vJx5PkiesjDd74RqqdaYG13gCerWnwkG0G+EzkmsFvRZwBxrb0s4PaaZGjhEf2SKCXu1ZTN8SLMwbZFLnvvnzsMvfDbtfjv31s7+zYQbtFuvuwqWAG/\x2f8yulskd/2TaDrtx/vBoaHTN4JGrbqLlwKuHh7QEy/Ao6UnM4HmZ67CzfqekJOZYNORXXviehSwN0brnEk54pBpk1vxURDJbb/U+S+dmY56BRU1R3hbkSoyI8KA/sVl5rAQ53hUiZnaBkm9mxxGPSXQbvUSODSTGGnlzBO9yOf8uXMAnZqKeicWUdtrWtB/aN53DCR6UW66UWEdQKt1+p51lLPINiprDqy3cpd6FIoOrgbxkyDmcZ7yy6sE7DPem1SQauhW3Xgu6CLIK+igteALdSpZA3NHj4gQ+i0AzwNC+woKnizl6KeAM4laAm4h7qmK6E/kDRRH10DfkGlumYUgMlkFXUIqU5krJC6EXk8ij6AX7WGg6sL0AcaS6E3Ia9Nozz+B/Ctpr4AvKDS0dmuRKOyytYIe21CHGNLfPjP8hXrf5SZd4NRInfBAAAAAElFTkSuQmCC");
/\x2f web/resources/vview-icon.png
var vview_icon_exports = {};
__export(vview_icon_exports, {
default: () => vview_icon_default
});
var vview_icon_default = __toBinary("iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAIAAADdvvtQAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGsmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDIxLTEwLTE2VDA2OjQwOjI2LTA1OjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMS0xMi0xMVQyMzo1MDowOS0wNjowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMS0xMi0xMVQyMzo1MDowOS0wNjowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDo4YjNhNjQ1Zi0xNjZjLWUyNDItODA0ZC0yMTUyNGYwY2IyMjYiIHhtcE1NOkRvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDo5YzAyOTMyZi05ZDY2LWEzNGItOTRiMy05YjRlMmVmMTdlYzkiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDpjMmNkZjUwMC0wZTAzLWI0NDQtYjEwNS0yZTMzNGQ1YTlmY2QiPiA8cGhvdG9zaG9wOlRleHRMYXllcnM+IDxyZGY6QmFnPiA8cmRmOmxpIHBob3Rvc2hvcDpMYXllck5hbWU9IlYiIHBob3Rvc2hvcDpMYXllclRleHQ9IlYiLz4gPHJkZjpsaSBwaG90b3Nob3A6TGF5ZXJOYW1lPSJWIiBwaG90b3Nob3A6TGF5ZXJUZXh0PSJWIi8+IDwvcmRmOkJhZz4gPC9waG90b3Nob3A6VGV4dExheWVycz4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDpjMmNkZjUwMC0wZTAzLWI0NDQtYjEwNS0yZTMzNGQ1YTlmY2QiIHN0RXZ0OndoZW49IjIwMjEtMTAtMTZUMDY6NDA6MjYtMDU6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6OGIzYTY0NWYtMTY2Yy1lMjQyLTgwNGQtMjE1MjRmMGNiMjI2IiBzdEV2dDp3aGVuPSIyMDIxLTEyLTExVDIzOjUwOjA5LTA2OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+fbDRPQAADClJREFUeJztnWtwVdUVx/\x2f7XN40vBIg4WXA8goQQMUyvBpBHgIjVltHpLVoQcWOY8c+hj6m0w+dsbWjM8iUWEKrjcIEq6gFKhG1LYRg5ZkACXkQIAQSCAGEAAmQc/rhttdwc5Pctfd9nL3P+n1kErLvOb+7z9pnrb22wJobYBhZrHgPgNEbFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRokO8B8B8RWqCmJ4iRvQSO6vtvGrn2q14DygMWCBX8NMJ1pIR1vhE8f9/sBqakHnYfjG/KZ7DCgPBTTbjy4DuInOG9WBq6Fji0ypnzpZbthPjQRHgGCiejO4tcmb7WrMHwKxB4r25vlgOiQoLFE8yZ/imp4i2f+ahodYv73bvbXLvyIwnK8P3zQHt2OPnt/f6MsL7ydjDAsWHpaOsZaMJF/9nE136IGOB4sOP0mlX/oEh4rmxbrxZbhyT8ayc2HzFHi5LhrvxZrlxTGaT0BHPjJG57FOShc99gRALFGteSLdSEyRF6Oy+QIgFiimWwGNfl7zm+2vdmNxggWLK8+OsMX0kp5/1ZXZkBxMRWKCY8rhsILypwn61gAXyNg+mWvf2k5l+PjxuP5Lr0qyqZtn4CUlieoro3RmV9dh5xjl22cVpxhZMay9rEZKN5fZj211qD3QRaGqyWJhqLRttJXW57d8L65zMI/brR9w4twcxsLt4Oo083+eU24tdbA+0EGjlROulyaHXr+mJInOGLz1RPLfD1VcZwHdHiJ6daL+y5aTjcnvgfoHen+d7aGg7X9wVY6wTV5yXD7h6HnpkGG36aWzCj11fTQaXB9G5Czu0a4+f30/2ufNNv59J/cQkYvj86z1NpZc0iPDce9GzZ/nmDCZc9BWuzDX6WXgHbWyFdW6fUAO49KI/k2Z9bwRtbFOTxcqJLv04Dw+jTT9vlephD1wr0A8otTIBfjPJN6KX6/KNGQPEWMrb54rLTqYO60o/bhSoiw/UiMFPZx+eHOW6TzRrEG1I68ucqzejNJbI47rLDSBNNlsEYOVES+59XfSgPr8+OK7N9AN3ClRVr7T6ePROF32oqckirTdBoPwaZ3+tBouvAC661gHOXUeJwgr2+XHWzIFumYTmDKY+v3SafuBOgQBsqlD6FkrX3EScRUMJKl+7hRwWKCJsPal0HZenWfOGxH8SGtVLkGqfc8rtC43RG05UcKlAu2qcn3+u9CKf+u4uGiy4gyZxXrVO0Y+f+F/l1vjdAVtlPfLDsVaY2/aix8PE/Jde6y8/7hUIwLe2NeWUy1/T+E5CPTphSjJt/XVRt+cXXC4QgMXbm/52TNKhn0yw7ukbt0loAVHfd2Q/Znxxu0AAXsy3pb+a1IdIBJlGmX4AbD2pXwAELQSqqnd+9YVkQP1CunVnj/hMQqRlYEGdU/4lCxQ11hy25R5k3TrgiZFx+IzjE8Uwirh/P6GlPdBFIAB/LpYMEZ4YKRI6RnYs7UOqZAKw84yWARA0Eij3lPOHgzJXOTVBxL7WLMxCSj/nG7C9imeg6LOu2G6UioUWxzaz0cmiLeA/qtR1+oFeApVecl7aL2PQhCTxrFRDDDnmDaH9LR1fQAfQSSAArx2y5Yo9vh/DUJpakKTpAt6PZgJdbMQfD8tM+JP7i2/Hqk5oPmUBv6/WOX2VBYohrxTYR6WqheYSV0ZyDO8pSP03Np/QOACCjgLdtLGuSOaiLxtttdtTVx2qpv8+o/H0Ax0FAvBKgb1Pqu5zfvTTq6QA6HwD/sUCxQW5nVNPp1kDu0d3EppLqWHNPaX38wv6CrSqUGYS6tMZS0dFUaCZA0WvzoSf13oB70dXgSA7CUV1zwY1xso9xQLFD7lJKD1RyG17DYeFrR+b0pLCOue4Vg2yQqKxQJCdhJ6Kzu7Vnp1Aql/brG0Gvjl6C7Sq0D54nnwbpiSLGVEol55N3AKWV6N9BA3dBQKwQWoj1TzizQ4Hagnix/oHQDBAoNWH7DJ6Ld/yNCulW4RHQqoB2lXjuPkcwvDRXqCGJmSXkCehpC4R7uMxrIcYTdkD/66eJfQt0V4gAJlH7DP0fOSjES0Sun+Q5xbwfkwQqK4BWcXk+zE+USyN3CREymCUfekUX2SB3MRfS+x6elMm0pmBbTOL0g9kuynTD4wR6PhlZ/UhclQxNVlEZOfhPX3FAEqKLa+GBXIf70ttLF9EqX5vjZnEAOhj/XOoAcwRaM85R6JY8fHhQv0Ut0WUDMbeWqeuQfUvugdzBAKwjb69YVgPsVwtEvIJ2h6MLZqXIAZhlEBbTjr59PBiCbEhdRCziM+vnfqXcDTHKIEArKNvYJ3cX1AlaM60FNo11L0EMQjTBHrjqH2Anl69n9jKuTkLKHswjMlgBDBNIAASPameHGX16yrztxK74C7KiwBjMhgBDBRobZF9irj5sH9XyL2Vvm8g7bf03QPfGgYKdKkR2SX01JhUqSuphOPYZefIBRZIB96tID8p7u4rJNqZkTqam1EAFISZAh08L5PZoFaEJXfDOEobaAP2YLTETIEAfHaafLdWjLWG9yQIkTGAGgCZFkHDYIE+OG7vOUdzqIsPi4cTBCKVcOyvdWqvk4ajB8YKBKn1/HcoofR9lABoi849XNrAZIGyiuyTV2i3bWyfcKvMkruBdI5TXrWBzy+YLdCVm3i7lPy9D7PKjPoGSCIm0wKTBQKwmX7qT5hVZqQlW36N02SmP6YL9J+zzi56fj6cKjNS/lWu2E0LDBcIwFp6N6p2q8wGfU2MpBwPberzC14QKLvELqij3b92q8wyKDujq+o1OwaVhPkCAXiHvp5vu8qM9AboE+MSqM3xhEDZpeSjuNquMiPNQCbtwWiJJwSqqnf+RI+EWqsy698VpADIpF1gLfGEQAA+o+ehWqsym05JgR2+4FRKdUbXBa8ItL3K2X02MlVmpDdA2ypNtgfeEQjA2/R2ZiGrzEhn+ZodAMFTAmUetkuJLe5bVpkldMSEJIJAn5pYwtEcDwnkADnl5Pkg6IFF6sO6t9aRaPmgFx4SCMDGcpuakwqqMiPtAss1PQCC1wQqukjePx9UZTab0sfOjDaabeMtgQDsoG8MDYTSXTvQGvnqfpBKOHhOoPcq7L3EzNSYPmJ5mgXgG/1oAdD1W7Sx6YjnBAKQQ+8MvHSkBWIKzAsBELwpUFYxudTV35v8AcphqF4IgOBNgS7fkCl1nTfYIvUB+qe5NUDN8aJAADbRSwSfovSh+uKcI3dCuXZ4VKD9tc7rR2gO9ae07/jIGwEQPCsQotzoKd8bARC8LNDGcsmDV8Nht+k51ADeFQjARnqpazgU1DlXTE+BBfC0QFnF5FZU4eCd6QceF0iuFVW7GNnGpTU8LRCAD6PQtZla+qg1XhdozzlHYudhG1RfQ4X+R+mGj9cFglR+vg28s4D3wwJhfZkdwZ2jn3vp+QUWyM/GyLVvlmhzrjUsEACsK7JP0w/NDInEMeRawwIBwIVGvHk0Aje+qt6oo5zCgQX6HxKtpVvitecXWKAAB887r9FbSwf/J3URGYtOsEBfod6HxWsBEFig5mw+YSsuwvcSO1MbAAt0G28clX+KVdYb3ogjJCzQbawtki8SiuwbbV1ggYJ5i97Ew4+nkvABWKBgVhWSm3ICOHrJ+YvC409fWKAQbKDvPFx9yL7pRX9YoFC8fIC2/XlDmb2G2LPBGFig0GSFXST0j0pnySfe2AMWChYoNGuL7OyS9h1aV2wv2OqBFgqt0yHeA4gYloDjIIILoWd3NCV1FfNbORZ+91lnbZH9picD5+YIrLkR7zGo4r/DUVpD/+Iua1qKNT1FNDSh+qqzr9bZWe3k1TjUdoumYoJATBzhGIhRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCVYIEYJFohRggVilGCBGCX+C3RktH6WMZYaAAAAAElFTkSuQmCC");
/\x2f web/resources/ppixiv.woff
var ppixiv_exports = {};
__export(ppixiv_exports, {
default: () => ppixiv_default
});
var ppixiv_default = __toBinary("d09GRgABAAAAAA6QAAwAAAAADkAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABHAAAAQQAAAEE3L3f2U9TLzIAAAIgAAAAYAAAAGAPahxyY21hcAAAAoAAAAE4AAABOOq+w9xnYXNwAAADuAAAAAgAAAAIAAAAEGdseWYAAAPAAAAH5AAAB+SeTEvnaGVhZAAAC6QAAAA2AAAANiE1UAdoaGVhAAAL3AAAACQAAAAkB18Di2htdHgAAAwAAAAAjAAAAIwv0gLgbG9jYQAADIwAAABIAAAASBCQEnRtYXhwAAAM1AAAACAAAAAgACwAmG5hbWUAAAz0AAABegAAAXrO/VPCcG9zdAAADnAAAAAgAAAAIAADAAAAAQAAAAoAHgAsAAFsYXRuAAgABAAAAAAAAAABAAAAAWxpZ2EACAAAAAEAAAABAAQABAAAAAEACgAAAAEAEgAGACIAMgBAAFAAbACyAAEABgAHAA4AEgAUABUAGAABAAQAHgAFABEAEQAVAAsAAQAEABwABAAMABAADQABAAQAIgAFAAwAGQAMABcAAQAEACEACwAWAAoACgAJABQAFQAMABEAEAAUAAQACgAaACgAPgAdAAcAGAAMABUAFQAJABMAHwAGABgADAAVAAgACwAbAAoACwAWAA8ABwAQAAYADAAOABQAIAADAAYACgABAAQAGgAJAAsABgAVABQABQAQAAkAGAADA/wBkAAFAAACmQLMAAAAjwKZAswAAAHrADMBCQAAAAAAAAAAAAAAAAAAAAEQAAAAAAAAAAAAAAAAAAAAAEAAAP/\x2fA2b/ZwCZA2YAmQAAAAEAAAAAAAAAAAAAACAAAAAAAAYAAAADAAAANAAAAAQAAACkAAEAAwAAADQAAQAEAAAApAADAAEAAAA0AAMACgAAAKQABABwAAAAGAAQAAMACAABACAAMABfAGMAZQBpAHAAeOkH/\x2f3/\x2fwAAAAAAIAAwAF8AYQBlAGcAawBy6QD/\x2ff/\x2fAAH/4/\x2fU/6b/pf+k/6P/ov+hFxoAAwABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAAAAJQAAAAAAAAACwAAAAAAAAABAAAAAQAAACAAAAAgAAAAAwAAADAAAAAwAAAABAAAAF8AAABfAAAABQAAAGEAAABjAAAABgAAAGUAAABlAAAACQAAAGcAAABpAAAACgAAAGsAAABwAAAADQAAAHIAAAB4AAAAEwAA6QAAAOkHAAAAGgAB8ysAAfMrAAAAIgABAAH/\x2fwAPAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAABAAJ/6ED9wNfABQAKQAtADEAAAEnNy8BBycPARcHFwcfATcXPwEnNwcXDwEnBy8BNyc3Jz8BFzcfAQcXBwUzFSMRMxEjA/dvD6VWnJxWpQ9vbw+lVpycVqUPb84MfkF2dkF+DFVVDH5BdnZBfgxVVf6pXFxcXAGAf6klkkNDkiWogH+pJZJEQ5EmqH9ggBxvMzNvHIBgYX8cbzMzbh1/YWApWwHJ/u0ABACgACADYALgAAMACAANABIAABMhESEBIREhEQEhESERKQERIRGgASX+2wGbASX+2/5lASX+2wGbASX+2wLg/tsBJf7bASX+Zf7bASX+2wElAAAAAwBT/9MDrQMtABoANQA5AAABJwcXFhQHBiIvAQcXFhcWMjc2NzY3NjQnJicFJyY0NzYyHwE3JyYnJiIHBgcGBwYUFxYfATcTAQcBA3OUS5UuLi6DLpRKlCYxMGUwMSYnExMTEyf9+ZUuLi6DLpRKlCYxMGUwMSYnExMTEyeUSyUBKUv+1wGAlEqULoMuLi6VS5QnExMTEycmMTBlMDEmSpQugy4uLpVLlCcTExMTJyYxMGUwMSaUSgEE/tdLASkAAQAoAAgD2AL4AEcAADceATMyNjcuASceATMyNjcuATU8ATUeARcuATU0NjcWFx4BFxYXLgE1NDYzMhYXPgE3DgEHPgE3DgEHHAEVFAcOAQcGIyImJygLFwxEezJAYxIJEgkNGgxCWRMtGCcwDg0jLSxmODk8AwJxUSlKGiE+HAssHh04GhMxHCQkjGdnhVKXQF0BASsmAUo5AgIEAw1oRQEBAQsMARlTMhowFSsjIzQPDwMKFgtPbyAcBhgQITYSBA8LHTEUBg0GXmBgnDExLSgAAAABAED/wAPAA0AAJQAAATUDIwcnIwMRIxUjFTMRFBY7AREzNTQ2NzMyFx4BFxYdATMRMzUDb607Xl0znWtRUR4VNW1hRgE4MjJKFhVQUQHnRQEU4sL+jwFfIGz+yBQe/qicQVsBFBRGLi41OgE1qgAABABf/5UDcwLMAAkAEQAVABkAABMHETMVMzczNxEFIREHIwc1IzczNSMXMzUjlTbFbGuh1/1qAk59xWuhxEhIxUhIAsyP/cNra9cB9Uf+dn1sbKHW1tYAAAEAUP/gA5ADIAASAAATASEHJiIHBhQXFjI3NjQnNxEBUAHAAYCAEjwSEhISPBISEoD+QAFgAcCAEhISPBISEhI8EoD+gP5AAAgAUf+ZA68DUwANACMAMgBAAFIAZAByAJUAACUjIiY1NDY7ATIWFRQGByMiBhUUFjsBFBY7ATI2NTMyNjU0JgMiJj0BNDYzMhYdARQGIwEjIiY1NDY7ATIWFRQGNyImLwEmNDc2Mh8BFhQHDgEjISImJyY0PwE2MhcWFA8BDgEjFyMiJjU0NjsBMhYVFAYHPgE1NCcuAScmIyIHDgEHBhUUFhceARUUFjsBMjY9AT4BNwI+fAwSEgx8DBISDHwMEhIMEBINHg0SEAwSEkoNEhINDRISDf7rew0SEg17DRISQAYLBVcJCQkZCVgJCQULBgGQBgsFCQlXChkJCQlXBQsGyHsNEhINew0SEv4aHxEROycnLS0nJzsRER0ZCzsSDHwMEgE0DhQSDQwSEgwNEh8SDQwSDRISDRIMDRICphIMfAwSEgx8DBL+6hINDRISDQ0SyQQFVwkZCQkJVwkZCQUEBAUJGQlXCQkJGQlXBQTJEg0NEhINDRJyHUoqLScnOxERERE7JyctKEkcDVMoDRISDQIlTRAAAAIAfAAiA4QC3gAqAD4AAAEeARUwFDkBFAYHDgEjIiYxFR4BByMmNjcRDgEHFgYxJzA3PgE3NjMyFhcDPgE1NCYnLgEjIgYHER4BMzI2NwMkLDQ4Li97Rk98Dh0Ojg4cDyQnCgsYMR8gclBQYUt9LkwhIyEfIFs7MW0lImQ9bSUgAosnbT8BP2wlJSgmewQRDg0SBAIMHC8SIgpPHx9LHx8sKP6HIFQzNVkiISkeGv53ERUmIAAAAAABAAAAAQAAnDXidV8PPPUACwQAAAAAAN8PhgsAAAAA3w+GCwAA/5UD9wNfAAAACAACAAAAAAAAAAEAAANm/2cAAAQAAAAAAAP3AAEAAAAAAAAAAAAAAAAAAAAjBAAAAAAAAAAAAAAABAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAJBAAAoAQAAFMEAAAoBAAAQAPSAF8EAABQBAAAUQQAAHwAAAAAAAoAFAAeACgAMgA8AEYAUABaAGQAbgB4AIIAjACWAKAAqgC0AL4AyADSANwA5gDwAPoBTgF4AdgCQgJ6AqYCygOWA/IAAQAAACMAlgAIAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAYAAAABAAAAAAACAAcAVwABAAAAAAADAAYAMwABAAAAAAAEAAYAbAABAAAAAAAFAAsAEgABAAAAAAAGAAYARQABAAAAAAAKABoAfgADAAEECQABAAwABgADAAEECQACAA4AXgADAAEECQADAAwAOQADAAEECQAEAAwAcgADAAEECQAFABYAHQADAAEECQAGAAwASwADAAEECQAKADQAmHBwaXhpdgBwAHAAaQB4AGkAdlZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMHBwaXhpdgBwAHAAaQB4AGkAdnBwaXhpdgBwAHAAaQB4AGkAdlJlZ3VsYXIAUgBlAGcAdQBsAGEAcnBwaXhpdgBwAHAAaQB4AGkAdkZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
/\x2f web/resources/heart-icon.svg
var heart_icon_default = '<svg viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http:/\x2fwww.w3.org/2000/svg" fill="#000000" style="--darkreader-inline-fill: var(--darkreader-background-000000, #000000);" data-darkreader-inline-fill="">\\n <g id="SVGRepo_bgCarrier" stroke-width="0"></g>\\n <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>\\n <g id="SVGRepo_iconCarrier">\\n <path d="M927.4 273.5v-95.4h-87.9V82.8h-201v95.3h-87.9v95.4h-78.5v-95.4h-88V82.8H183.2v95.3H95.3v95.4H16.7v190.6h78.6v95.4h75.3v95.3H246v95.3h87.9v95.4h100.5v95.3h153.9v-95.3h100.4v-95.4h88v-95.3H852.1v-95.3h75.3v-95.4h78.5V273.5z" fill="#E02D2D" style="--darkreader-inline-fill: var(--darkreader-background-e02d2d, #aa1919);" data-darkreader-inline-fill=""></path>\\n </g>\\n</svg>';
/\x2f web/resources/pan-editor-marker.svg
var pan_editor_marker_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg"\\n width="100%" height="100%"\\n viewBox="0 0 1 1"\\n>\\n <g class="crosshair" style="\\n fill: #fff;\\n paint-order: stroke;\\n stroke: #000;\\n stroke-width: 5;\\n ">\\n <path d="\\n M -25 -2.5 h 50 v 5 h -50 Z\\n M -2.5 -25 v 50 h 5 v -50 Z\\n "/>\\n </g>\\n</svg>\\n';
/\x2f web/resources/multi-monitor.svg
var multi_monitor_default = '<svg\\n xmlns="http:/\x2fwww.w3.org/2000/svg"\\n xmlns:xlink="http:/\x2fwww.w3.org/1999/xlink"\\n width="580px" height="250px" viewBox="0 0 580 250" fill="#ff0;">\\n <defs>\\n <g id="monitors" fill="#fff" stroke-linejoin="round">\\n <g transform="translate(20 65)">\\n <path d="M 0 -5 L 160 0 L 160 90 L 0 95 L 0 0 z" />\\n </g>\\n <g transform="translate(210 65)">\\n <path d="M 0 0 H 160 V 90 H 0 V 0 z" />\\n </g>\\n <g transform="translate(400 65)">\\n <g class="rotating-monitor" transform-origin="50 50">\\n <path d="M 0 0 L 160 -5 L 160 95 L 0 90 L 0 0 z" />\\n </g>\\n </g>\\n </g>\\n </defs>\\n <g stroke-width="18" stroke="#ccc">\\n <use xlink:href="#monitors" />\\n </g>\\n <g stroke-width="10" stroke="#000">\\n <use xlink:href="#monitors" />\\n </g>\\n</svg>\\n';
/\x2f web/resources/regular-pixiv-icon.png
var regular_pixiv_icon_exports = {};
__export(regular_pixiv_icon_exports, {
default: () => regular_pixiv_icon_default
});
var regular_pixiv_icon_default = __toBinary("iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAMAAABlApw1AAAAG1BMVEX/\x2f/8Alvqe1/1SuPwYoPvn9v/F5/40rPt2x/37JN9BAAAEBElEQVR42u3c23YsERAG4EJR3v+J92SSycqe0d2UYkh+F7nKEl+jHAJymycCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBVACwhe59SjJGIbj9TSj4H4fUBfCt5ouOUfJBlAZx9pJqUsqwHCJWF/0rRh4UA3Fj6h0GWAHBIpE0pvB0gmm/\x2fsxryOwGczz/+PX5+htKTXwrvArA/ad63oP+zhctZdFUTqK/tHJZdjvv6ASLxbIAkZYCXHO26AtkWP+W671iOWppKIMO2HzN3Nj9FTyCz4jfH82Id5hmAUhNOmhE1FwR+OKD04ZJyQsCpuyOQwVfrGUlzr6AREKJZAH/k2JkhdXde6pwTS+wSUN+fMgAUBUMAR9Oe7lVJQeDtAXI4D+tfVknHeEAdvdcMUOrJYgvIJ5N5A0Ah/2gJ4NM1iwXAJW0jovZOlvwAAEdlI6LmBprdCIC6EVFrzsGNAWgb0SXg/9JGcaMAr7E0sgEgvZZ/EKAwUOZ+QCp9kkEAVlUBNYTPR36DALoqoPbyDwOIJhBRffnFDQYUAlHoADwPv+KGA4JiXk3149d4gHudp3BHE/JHE/RxAEU3prrpSXRTALm9DVFddjIHwO1tiKqigndzAIo4RDWB+WlEHAjwzatjqsnPu1mA9kB6MReKpUwGAgrL+z5AKGUyEOCaV/dU1Sin9QEXW3vxFeBzMJBpgOZ12eWKLM+tgeYwRDXfZF4nbh+LqSIwPG8Wbwa4rObBgLgXIPw+AAEAwJ/qxL8QsNk4MGAqMReQdgeYT6cnA9h8QTMZEKyXlLMB3npRPxuQjLdVZgOYbDe2pgOC8dbidECy3dydDlC0oLUAWfF/yqUAmoNPKwFUp4ZWAii68FIA3bGtdQCsO7y4DsDrzs0tAxDlOfBlAFF3XGgZgNceJlwEkNUXCdYAFCJo5I0AhQ5sfXJ3KKB0Kt787PRAQFf5FwCEvptAbwf03mR6M6B4rDy5bQDFWwltN8kMAOor2eVLIY13ugwASsLBVeTWrEwAH/UeTIofm68EWgHujyxUX/86uknt2wtjB/h6s4MrSh8Nr6XbAu7FSCcPqMjJwx9edaXRHvBgeJ9zCCLCtyQS8sWDK8oLsaMArU9LqAcTA0B8Y/FNANz5skff2yQWgHvnVD6u0v06jBHANbwsZPcwjC3gKkr2DHzTAPeaCD7Fy8Jnq0eq7AFfio8HwkoX8JNd2UcCvtuUhNsI9pHCLVm/cDYBMD4BAAAAAAAAAAAAAPC3AM+bypl3AnDpkTkv2wBytNwWnA8Ix/sLewBONn9kC8DJ+668BYD9UQq7RKHlEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADQnP4BoAuIrpag2iEAAAAASUVORK5CYII=");
/\x2f web/resources/download-icon.svg
var download_icon_default = '<svg viewBox="0 0 24 24" fill="none" xmlns="http:/\x2fwww.w3.org/2000/svg">\\n <g id="SVGRepo_bgCarrier" stroke-width="0"></g>\\n <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>\\n <g id="SVGRepo_iconCarrier">\\n <path fill-rule="evenodd" clip-rule="evenodd" d="M2 12C2 7.28595 2 4.92893 3.46447 3.46447C4.92893 2 7.28595 2 12 2C16.714 2 19.0711 2 20.5355 3.46447C22 4.92893 22 7.28595 22 12C22 16.714 22 19.0711 20.5355 20.5355C19.0711 22 16.714 22 12 22C7.28595 22 4.92893 22 3.46447 20.5355C2 19.0711 2 16.714 2 12ZM12 6.25C12.4142 6.25 12.75 6.58579 12.75 7V12.1893L14.4697 10.4697C14.7626 10.1768 15.2374 10.1768 15.5303 10.4697C15.8232 10.7626 15.8232 11.2374 15.5303 11.5303L12.5303 14.5303C12.3897 14.671 12.1989 14.75 12 14.75C11.8011 14.75 11.6103 14.671 11.4697 14.5303L8.46967 11.5303C8.17678 11.2374 8.17678 10.7626 8.46967 10.4697C8.76256 10.1768 9.23744 10.1768 9.53033 10.4697L11.25 12.1893V7C11.25 6.58579 11.5858 6.25 12 6.25ZM8 16.25C7.58579 16.25 7.25 16.5858 7.25 17C7.25 17.4142 7.58579 17.75 8 17.75H16C16.4142 17.75 16.75 17.4142 16.75 17C16.75 16.5858 16.4142 16.25 16 16.25H8Z" fill="#ffffff" style="--darkreader-inline-fill: var(--darkreader-background-ffffff, #181a1b);" data-darkreader-inline-fill=""></path>\\n </g>\\n</svg>';
/\x2f web/resources/download-manga-icon.svg
var download_manga_icon_default = '<svg viewBox="0 0 24 24" fill="none" xmlns="http:/\x2fwww.w3.org/2000/svg">\\n <g id="SVGRepo_bgCarrier" stroke-width="0"></g>\\n <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>\\n <g id="SVGRepo_iconCarrier">\\n <path fill-rule="evenodd" clip-rule="evenodd" d="M3.46447 3.46447C2 4.92893 2 7.28595 2 12C2 16.714 2 19.0711 3.46447 20.5355C4.92893 22 7.28595 22 12 22C16.714 22 19.0711 22 20.5355 20.5355C22 19.0711 22 16.714 22 12C22 7.28595 22 4.92893 20.5355 3.46447C19.0711 2 16.714 2 12 2C7.28595 2 4.92893 2 3.46447 3.46447ZM9.25 7C9.25 6.58579 8.91421 6.25 8.5 6.25C8.08579 6.25 7.75 6.58579 7.75 7V11.9285L6.57617 10.5199C6.31099 10.2017 5.83807 10.1587 5.51986 10.4238C5.20165 10.689 5.15866 11.1619 5.42383 11.4801L7.92383 14.4801C8.06633 14.6511 8.27742 14.75 8.5 14.75C8.72259 14.75 8.93367 14.6511 9.07617 14.4801L11.5762 11.4801C11.8413 11.1619 11.7983 10.689 11.4801 10.4238C11.1619 10.1587 10.689 10.2017 10.4238 10.5199L9.25 11.9285V7ZM15.5 6.25C15.9142 6.25 16.25 6.58579 16.25 7V11.9285L17.4238 10.5199C17.689 10.2017 18.1619 10.1587 18.4801 10.4238C18.7983 10.689 18.8413 11.1619 18.5762 11.4801L16.0762 14.4801C15.9337 14.6511 15.7226 14.75 15.5 14.75C15.2774 14.75 15.0663 14.6511 14.9238 14.4801L12.4238 11.4801C12.1587 11.1619 12.2017 10.689 12.5199 10.4238C12.8381 10.1587 13.311 10.2017 13.5762 10.5199L14.75 11.9285V7C14.75 6.58579 15.0858 6.25 15.5 6.25ZM6 16.25C5.58579 16.25 5.25 16.5858 5.25 17C5.25 17.4142 5.58579 17.75 6 17.75H18C18.4142 17.75 18.75 17.4142 18.75 17C18.75 16.5858 18.4142 16.25 18 16.25H6Z" fill="#ffffff" style="--darkreader-inline-fill: var(--darkreader-background-ffffff, #181a1b);" data-darkreader-inline-fill=""></path>\\n </g>\\n</svg>';
/\x2f web/resources/last-viewed-image-marker.svg
var last_viewed_image_marker_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg"\\n viewBox="8 0 32 32"\\n width="32" height="32"\\n fill="#FF0"\\n stroke="#000"\\n stroke-width="1">\\n <g transform-origin="16 16" transform="rotate(-45, 0, 0)">\\n <rect x="-100" y="16" width="200" height="4" fill="#FFF" />\\n </g>\\n</svg>\\n';
/\x2f web/resources/last-page.svg
var last_page_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg" viewBox="0 0 16 32" width="64" height="128"\\n class="last-page"\\n>\\n <defs>\\n <g id="foo">\\n <g id="last-page-p1" fill="none" >\\n <path d="M2 6 l 0 20" />\\n </g>\\n <g id="last-page-p2" fill="none" >\\n <path d="M14 8 l -8 +8 l +8 +8" />\\n </g>\\n </g>\\n </defs>\\n \\n <style>\\n .last-page .outline {\\n stroke: #000;\\n stroke-width: 3;\\n }\\n .last-page .fill {\\n stroke: #fff;\\n stroke-width: 2;\\n }\\n </style>\\n\\n <g\\n stroke-linecap="round"\\n stroke-linejoin="round"\\n transform="scale(1 1)"\\n transform-origin="center center"\\n >\\n <!-- | -->\\n <g class="bar">\\n <use href="#last-page-p1" class="outline" />\\n <use href="#last-page-p1" class="fill" />\\n </g>\\n\\n <!-- < -->\\n <g class="animated" transform="translate(0 0)">\\n <use href="#last-page-p2" class="outline" />\\n <use href="#last-page-p2" class="fill" />\\n </g>\\n </g>\\n</svg>\\n';
/\x2f web/resources/icon-twitter.svg
var icon_twitter_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg"\\n viewBox="0 -1 32 32"\\n width="32" height="32"\\n style="fill: currentColor;"\\n>\\n <path d="M3,24.3115115 C3.43335894,24.361638 3.8743224,24.3872818 4.32140898,24.3872818 C6.9166246,24.3872818 9.30513552,23.5195519 11.2009327,22.063852 C8.77696697,22.0200155 6.73125206,20.4508052 6.02640184,18.2943991 C6.36455613,18.3577829 6.71159881,18.391749 7.0685175,18.391749 C7.57377373,18.391749 8.0631296,18.325462 8.52799298,18.2014038 C5.99381104,17.7028486 4.0844837,15.5090896 4.0844837,12.8792882 C4.0844837,12.8564507 4.0844837,12.8337099 4.08487874,12.8110659 C4.83180072,13.2175936 5.68587732,13.4617424 6.59397564,13.4899022 C5.10763744,12.5165971 4.12961703,10.8552624 4.12961703,8.97222947 C4.12961703,7.97753828 4.40278723,7.04516649 4.87960057,6.24352996 C7.61169757,9.52729793 11.6933502,11.6881554 16.2971469,11.9144987 C16.2027324,11.5172609 16.1536486,11.1029917 16.1536486,10.6775941 C16.1536486,7.68016635 18.6339076,5.25 21.6931965,5.25 C23.2863931,5.25 24.7260178,5.90919218 25.7365302,6.96407385 C26.9982882,6.7206024 28.1838034,6.26888351 29.2540656,5.64694749 C28.8404587,6.91433445 27.9621859,7.97802213 26.8184462,8.64979431 C27.9389773,8.5185752 29.006573,8.22681589 30,7.79512823 C29.2573247,8.88358883 28.318117,9.83957232 27.2359048,10.6048236 C27.2465708,10.8375537 27.2519039,11.0716385 27.2519039,11.3069813 C27.2519039,18.4794219 21.680259,26.75 11.4913859,26.75 C8.36316252,26.75 5.45142105,25.8514974 3,24.3115115 Z"></path>\\n</svg>\\n\\n';
/\x2f web/resources/zoom-actual.svg
var zoom_actual_default = '<svg viewBox="0 0 32 32" width="32" height="32" xmlns="http:/\x2fwww.w3.org/2000/svg">\\n <defs>\\n <pattern id="checker" viewBox="0,0,6,6" width="6px" height="6px" patternUnits="userSpaceOnUse">\\n <polygon points="0,0 0,3 3,3 3,0 0,0" fill="currentColor" />\\n <polygon points="3,3 3,6 6,6 6,3 3,3" fill="currentColor" />\\n </pattern>\\n </defs>\\n\\n <line x1="20.3" y1="20.3" x2="28" y2="28" stroke="currentColor" stroke-width="2.5" />\\n <circle cx="14" cy="14" r="8" fill="url(#checker)" stroke="currentColor" stroke-width="2" />\\n</svg>\\n\\n';
/\x2f web/resources/play-button.svg
var play_button_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" fill="#444" stroke="#eee">\\n <path d="M26 16 L 8 4 L 8 28 L 26 16"/>\\n</svg>\\n\\n';
/\x2f web/resources/followed.svg
var followed_default = '<svg viewBox="0 0 24 24" fill="none" xmlns="http:/\x2fwww.w3.org/2000/svg">\\n <g id="SVGRepo_bgCarrier" stroke-width="0"></g>\\n <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>\\n <g id="SVGRepo_iconCarrier">\\n <path d="M10.8613 9.36335C11.3679 8.45445 11.6213 8 12 8C12.3787 8 12.6321 8.45445 13.1387 9.36335L13.2698 9.59849C13.4138 9.85677 13.4858 9.98591 13.598 10.0711C13.7103 10.1563 13.8501 10.188 14.1296 10.2512L14.3842 10.3088C15.3681 10.5314 15.86 10.6427 15.977 11.0191C16.0941 11.3955 15.7587 11.7876 15.088 12.572L14.9144 12.7749C14.7238 12.9978 14.6285 13.1092 14.5857 13.2471C14.5428 13.385 14.5572 13.5336 14.586 13.831L14.6122 14.1018C14.7136 15.1482 14.7644 15.6715 14.4579 15.9041C14.1515 16.1367 13.6909 15.9246 12.7697 15.5005L12.5314 15.3907C12.2696 15.2702 12.1387 15.2099 12 15.2099C11.8613 15.2099 11.7304 15.2702 11.4686 15.3907L11.2303 15.5005C10.3091 15.9246 9.84847 16.1367 9.54206 15.9041C9.23565 15.6715 9.28635 15.1482 9.38776 14.1018L9.41399 13.831C9.44281 13.5336 9.45722 13.385 9.41435 13.2471C9.37147 13.1092 9.27617 12.9978 9.08557 12.7749L8.91204 12.572C8.2413 11.7876 7.90593 11.3955 8.02297 11.0191C8.14001 10.6427 8.63194 10.5314 9.61581 10.3088L9.87035 10.2512C10.1499 10.188 10.2897 10.1563 10.402 10.0711C10.5142 9.98591 10.5862 9.85677 10.7302 9.59849L10.8613 9.36335Z" stroke="#800" stroke-width="1.5" style="--darkreader-inline-stroke: var(--darkreader-text-880000, #ff6d6d);" data-darkreader-inline-stroke=""></path>\\n <path d="M7 3.33782C8.47087 2.48697 10.1786 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22C6.47715 22 2 17.5228 2 12C2 10.1786 2.48697 8.47087 3.33782 7" stroke="#800" stroke-width="1.5" stroke-linecap="round" style="--darkreader-inline-stroke: var(--darkreader-text-880000, #ff6d6d);" data-darkreader-inline-stroke=""></path>\\n </g>\\n</svg>';
/\x2f web/resources/manifest.json
var manifest_default = {
short_name: "vview",
display: "fullscreen",
scope: "/",
background_color: "#202020",
icons: [{
type: "image/png",
sizes: "192x192",
src: "/resources/vview-icon.png"
}]
};
/\x2f web/resources/picture-in-picture.svg
var picture_in_picture_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg" height="36px" viewBox="0 0 24 24" width="36px" fill="currentColor"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 11h-8v6h8v-6zm-2 4h-4v-2h4v2zm4-12H3c-1.1 0-2 .88-2 1.98V19c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V4.98C23 3.88 22.1 3 21 3zm0 16.02H3V4.97h18v14.05z"/></svg>\\n';
/\x2f web/resources/eye-icon.svg
var eye_icon_default = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\\n<svg\\n xmlns:dc="http:/\x2fpurl.org/dc/elements/1.1/"\\n xmlns:cc="http:/\x2fcreativecommons.org/ns#"\\n xmlns:rdf="http:/\x2fwww.w3.org/1999/02/22-rdf-syntax-ns#"\\n xmlns:svg="http:/\x2fwww.w3.org/2000/svg"\\n xmlns="http:/\x2fwww.w3.org/2000/svg"\\n xmlns:sodipodi="http:/\x2fsodipodi.sourceforge.net/DTD/sodipodi-0.dtd"\\n xmlns:inkscape="http:/\x2fwww.inkscape.org/namespaces/inkscape"\\n viewBox="0 0 32 32"\\n width="32"\\n height="32"\\n class="eye-image"\\n fill="currentColor"\\n version="1.1"\\n id="svg32"\\n sodipodi:docname="eye-icon.svg"\\n inkscape:version="0.92.3 (2405546, 2018-03-11)">\\n <metadata\\n id="metadata38">\\n <rdf:RDF>\\n <cc:Work\\n rdf:about="">\\n <dc:format>image/svg+xml</dc:format>\\n <dc:type\\n rdf:resource="http:/\x2fpurl.org/dc/dcmitype/StillImage" />\\n <dc:title />\\n </cc:Work>\\n </rdf:RDF>\\n </metadata>\\n <defs\\n id="defs36">\\n <mask\\n maskUnits="userSpaceOnUse"\\n id="mask1075">\\n <g\\n inkscape:label="eye-mask"\\n style="display:inline"\\n id="g1081">\\n <rect\\n y="1.5105144"\\n x="1.5820016"\\n height="28.811605"\\n width="29.818333"\\n id="rect1077"\\n style="fill:#020202;fill-opacity:1;stroke:none;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />\\n <path\\n inkscape:label="outline1-mask"\\n inkscape:connector-curvature="0"\\n id="path1079"\\n d="m 4.1355932,15.966102 c 0,0 5.4237288,-6.2033899 12.0000008,-6.1016949 6.576271,0.101695 11.559322,6.0338979 11.559322,6.0338979 0,0 -2.745763,6 -11.423729,6.101695 C 7.5932203,22.101695 4.1355932,15.966102 4.1355932,15.966102 Z"\\n style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#ffffff;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />\\n </g>\\n </mask>\\n <filter\\n inkscape:collect="always"\\n style="color-interpolation-filters:sRGB"\\n id="filter4556"\\n x="-0.074548502"\\n width="1.149097"\\n y="-0.1446944"\\n height="1.2893888">\\n <feGaussianBlur\\n inkscape:collect="always"\\n stdDeviation="0.73179676"\\n id="feGaussianBlur4558" />\\n </filter>\\n </defs>\\n <sodipodi:namedview\\n pagecolor="#ffffff"\\n bordercolor="#666666"\\n borderopacity="1"\\n objecttolerance="10"\\n gridtolerance="10"\\n guidetolerance="10"\\n inkscape:pageopacity="0"\\n inkscape:pageshadow="2"\\n inkscape:window-width="1920"\\n inkscape:window-height="1137"\\n id="namedview34"\\n showgrid="false"\\n inkscape:zoom="41.7193"\\n inkscape:cx="14.867981"\\n inkscape:cy="15.247435"\\n inkscape:window-x="1912"\\n inkscape:window-y="-8"\\n inkscape:window-maximized="1"\\n inkscape:current-layer="svg32" />\\n <rect\\n x="0"\\n y="0"\\n width="32"\\n height="32"\\n id="bg"\\n style="display:none;fill:#000088;fill-opacity:1"\\n inkscape:label="bg" />\\n <path\\n inkscape:label="shadow"\\n inkscape:connector-curvature="0"\\n id="shadow"\\n d="m 4.1355932,15.966102 c 0,0 5.4237288,-6.20339 12.0000008,-6.101695 6.576271,0.101695 11.559322,6.033898 11.559322,6.033898 0,0 -2.745763,6 -11.423729,6.101695 C 7.5932203,22.101695 4.1355932,15.966102 4.1355932,15.966102 Z"\\n style="display:inline;fill:none;fill-opacity:1;stroke:#fbfbfb;stroke-width:3.77952756;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter4556);stroke-miterlimit:4;stroke-dasharray:none" />\\n <path\\n inkscape:label="outline1"\\n inkscape:connector-curvature="0"\\n id="outline1"\\n class="outline1"\\n d="m 4.1355932,15.966102 c 0,0 5.4237288,-6.2033899 12.0000008,-6.1016949 6.576271,0.101695 11.559322,6.0338979 11.559322,6.0338979 0,0 -2.745763,6 -11.423729,6.101695 C 7.5932203,22.101695 4.1355932,15.966102 4.1355932,15.966102 Z"\\n style="display:inline;fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />\\n <g\\n id="eye"\\n inkscape:label="eye"\\n style="display:inline"\\n mask="url(#mask1075)">\\n <path\\n style="display:inline;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:1.37283087px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"\\n d="m 1.5948027,16.654121 c 0,0 6.825353,-9.2904337 15.1010963,-9.1381313 8.27574,0.1523023 14.546535,9.0365953 14.546535,9.0365953 0,0 -3.455336,8.985827 -14.375902,9.13813 C 5.9459657,25.843018 1.5948027,16.654121 1.5948027,16.654121 Z"\\n id="fill"\\n inkscape:connector-curvature="0"\\n inkscape:label="fill" />\\n <g\\n id="layer1"\\n class="middle"\\n inkscape:label="middle"\\n style="display:inline;opacity:1"\\n inkscape:groupmode="layer">\\n <path\\n style="fill:#00a8ff;fill-opacity:1;stroke:#000000;stroke-width:1.02918112px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"\\n d="m 15.980909,9.5250445 c -2.223727,-3.144e-4 -3.075583,3.5258445 -3.007786,6.8291555 0.05898,2.873784 0.921431,5.825346 2.991275,5.877994 2.369776,-0.04304 3.052389,-2.967868 2.974827,-5.98571 -0.08375,-3.258728 -0.510428,-6.7210934 -2.958316,-6.7214395 z"\\n id="middle-outline"\\n inkscape:connector-curvature="0"\\n sodipodi:nodetypes="sscss"\\n inkscape:label="middle-outline" />\\n <path\\n style="fill:#000012;fill-opacity:1;stroke:none;stroke-width:1.02918112px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"\\n d="m 15.939865,11.878838 c -0.822379,0.0033 -1.404285,1.981256 -1.404284,4.065998 10e-7,2.084742 0.499108,4.40718 1.459244,4.407179 0.960136,-1e-6 1.524881,-2.19813 1.527042,-4.398093 0.0022,-2.199963 -0.759623,-4.078404 -1.582002,-4.075084 z"\\n id="iris"\\n inkscape:connector-curvature="0"\\n sodipodi:nodetypes="zzzzz"\\n inkscape:label="iris" />\\n </g>\\n </g>\\n <path\\n inkscape:label="outline2"\\n inkscape:connector-curvature="0"\\n id="outline2"\\n d="m 4.1355932,15.966102 c 0,0 5.4237288,-6.2033901 12.0000008,-6.1016951 6.576271,0.101695 11.559322,6.0338981 11.559322,6.0338981 0,0 -2.745763,6 -11.423729,6.101695 C 7.5932203,22.101695 4.1355932,15.966102 4.1355932,15.966102 Z"\\n style="display:inline;fill:none;fill-opacity:1;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;font-variant-east_asian:normal;opacity:1;vector-effect:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0" />\\n</svg>\\n';
/\x2f web/resources/zoom-minus.svg
var zoom_minus_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" stroke-width="2" stroke="currentColor">\\n <circle cx="16" cy="16" r="10" fill="none" />\\n <line x1="12" y1="16" x2="20" y2="16" />\\n</svg>\\n';
/\x2f web/resources/page-icon.png
var page_icon_exports = {};
__export(page_icon_exports, {
default: () => page_icon_default
});
var page_icon_default = __toBinary("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFwmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDIgNzkuMTYwOTI0LCAyMDE3LzA3LzEzLTAxOjA2OjM5ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNi0zMFQwMjowMToxNy0wNTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOC0wNi0zMFQwMjowMToxNy0wNTowMCIgeG1wOk1vZGlmeURhdGU9IjIwMTgtMDYtMzBUMDI6MDE6MTctMDU6MDAiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NjE2Mzg1YjEtMDhiNS1kMjRhLWI0MzItNzAzYjBmNDFjNjE5IiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6YmNlMzU0ZTgtMDE3Zi1iMjRkLTg4MTYtOGZkZTZlYTgyZDg5IiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZTAzMzRhNzEtMmQ2Mi1lNDRjLWFiMjUtZGJjZTNlYTcwNTYwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyI+IDx4bXBNTTpIaXN0b3J5PiA8cmRmOlNlcT4gPHJkZjpsaSBzdEV2dDphY3Rpb249ImNyZWF0ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6ZTAzMzRhNzEtMmQ2Mi1lNDRjLWFiMjUtZGJjZTNlYTcwNTYwIiBzdEV2dDp3aGVuPSIyMDE4LTA2LTMwVDAyOjAxOjE3LTA1OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIi8+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJzYXZlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDo2MTYzODViMS0wOGI1LWQyNGEtYjQzMi03MDNiMGY0MWM2MTkiIHN0RXZ0OndoZW49IjIwMTgtMDYtMzBUMDI6MDE6MTctMDU6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+1Ie7qQAAADpJREFUOI1j/P/\x2fPwMlgIki3dQwgAWNT8g/jNRwAYolAx8GowZgpgOMeCbVAAYGwomJoAEkuWLgAxEAc7EGJRNwU4UAAAAASUVORK5CYII=");
/\x2f web/resources/followed-users-eye.svg
var followed_users_eye_default = '<svg viewBox="0 0 24 24" fill="none" xmlns="http:/\x2fwww.w3.org/2000/svg">\\n <g id="SVGRepo_bgCarrier" stroke-width="0"></g>\\n <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>\\n <g id="SVGRepo_iconCarrier">\\n <path d="M18 18.86H17.24C16.44 18.86 15.68 19.17 15.12 19.73L13.41 21.42C12.63 22.19 11.36 22.19 10.58 21.42L8.87 19.73C8.31 19.17 7.54 18.86 6.75 18.86H6C4.34 18.86 3 17.53 3 15.89V4.97998C3 3.33998 4.34 2.01001 6 2.01001H18C19.66 2.01001 21 3.33998 21 4.97998V15.89C21 17.52 19.66 18.86 18 18.86Z" stroke="#ffffff" stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round" style="--darkreader-inline-stroke: var(--darkreader-text-ffffff, #e8e6e3);" data-darkreader-inline-stroke=""></path>\\n <path d="M12.28 14.96C12.13 15.01 11.88 15.01 11.72 14.96C10.42 14.51 7.5 12.66 7.5 9.51001C7.5 8.12001 8.62 7 10 7C10.82 7 11.54 7.39 12 8C12.46 7.39 13.18 7 14 7C15.38 7 16.5 8.12001 16.5 9.51001C16.49 12.66 13.58 14.51 12.28 14.96Z" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="--darkreader-inline-stroke: var(--darkreader-text-ffffff, #e8e6e3);" data-darkreader-inline-stroke=""></path>\\n </g>\\n</svg>';
/\x2f web/resources/zoom-full.svg
var zoom_full_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" stroke-width="3" fill="none" stroke="currentColor">\\n <path d="M8 14 l 0 -6 l 6 0" />\\n <path d="M24 14 l 0 -6 l -6 0" />\\n <path d="M8 18 l 0 +6 l 6 0" />\\n <path d="M24 18 l 0 +6 l -6 0" />\\n</svg>\\n';
/\x2f web/resources/folder.svg
var folder_default = '<svg viewBox="0 0 24 24" fill="none" xmlns="http:/\x2fwww.w3.org/2000/svg">\\n <g id="SVGRepo_bgCarrier" stroke-width="0"></g>\\n <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>\\n <g id="SVGRepo_iconCarrier">\\n <path d="M18 10L13 10" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" style="--darkreader-inline-stroke: var(--darkreader-text-ffffff, #e8e6e3);" data-darkreader-inline-stroke=""></path>\\n <path d="M10 3H16.5C16.9644 3 17.1966 3 17.3916 3.02567C18.7378 3.2029 19.7971 4.26222 19.9743 5.60842C20 5.80337 20 6.03558 20 6.5" stroke="#ffffff" stroke-width="1.5" style="--darkreader-inline-stroke: var(--darkreader-text-ffffff, #e8e6e3);" data-darkreader-inline-stroke=""></path>\\n <path d="M22 11.7979C22 9.16554 22 7.84935 21.2305 6.99383C21.1598 6.91514 21.0849 6.84024 21.0062 6.76946C20.1506 6 18.8345 6 16.2021 6H15.8284C14.6747 6 14.0979 6 13.5604 5.84678C13.2651 5.7626 12.9804 5.64471 12.7121 5.49543C12.2237 5.22367 11.8158 4.81578 11 4L10.4497 3.44975C10.1763 3.17633 10.0396 3.03961 9.89594 2.92051C9.27652 2.40704 8.51665 2.09229 7.71557 2.01738C7.52976 2 7.33642 2 6.94975 2C6.06722 2 5.62595 2 5.25839 2.06935C3.64031 2.37464 2.37464 3.64031 2.06935 5.25839C2 5.62595 2 6.06722 2 6.94975M21.9913 16C21.9554 18.4796 21.7715 19.8853 20.8284 20.8284C19.6569 22 17.7712 22 14 22H10C6.22876 22 4.34315 22 3.17157 20.8284C2 19.6569 2 17.7712 2 14V11" stroke="#ffffff" stroke-width="1.5" stroke-linecap="round" style="--darkreader-inline-stroke: var(--darkreader-text-ffffff, #e8e6e3);" data-darkreader-inline-stroke=""></path>\\n </g>\\n</svg>';
/\x2f web/resources/fullscreen.svg
var fullscreen_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" stroke-width="2" fill="none" stroke="currentColor">\\n <path d="M 7 13 l 0 -6 l 6 0 m -6 0 l 5 5" />\\n <path d="M25 13 l 0 -6 l -6 0 m 6 0 l -5 5" />\\n <path d="M 7 19 l 0 +6 l 6 0 m -6 0 l 5 -5" />\\n <path d="M25 19 l 0 +6 l -6 0 m 6 0 l -5 -5" />\\n</svg>\\n';
/\x2f web/resources/like-button.svg
var like_button_default = '<svg viewBox="0 -0.5 21 21" version="1.1" xmlns="http:/\x2fwww.w3.org/2000/svg" xmlns:xlink="http:/\x2fwww.w3.org/1999/xlink" fill="#000000" style="--darkreader-inline-fill: var(--darkreader-background-000000, #000000);" data-darkreader-inline-fill="">\\n <g id="SVGRepo_bgCarrier" stroke-width="0"></g>\\n <g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>\\n <g id="SVGRepo_iconCarrier">\\n <title>like [#ffffff]</title>\\n <desc>Created with Sketch.</desc>\\n <defs> </defs>\\n <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" style="--darkreader-inline-stroke: none;" data-darkreader-inline-stroke="">\\n <g id="Dribbble-Light-Preview" transform="translate(-219.000000, -760.000000)" fill="#ffffff" style="--darkreader-inline-fill: var(--darkreader-background-ffffff, #181a1b);" data-darkreader-inline-fill="">\\n <g id="icons" transform="translate(56.000000, 160.000000)">\\n <path d="M163,610.021159 L163,618.021159 C163,619.126159 163.93975,620.000159 165.1,620.000159 L167.199999,620.000159 L167.199999,608.000159 L165.1,608.000159 C163.93975,608.000159 163,608.916159 163,610.021159 M183.925446,611.355159 L182.100546,617.890159 C181.800246,619.131159 180.639996,620.000159 179.302297,620.000159 L169.299999,620.000159 L169.299999,608.021159 L171.104948,601.826159 C171.318098,600.509159 172.754498,599.625159 174.209798,600.157159 C175.080247,600.476159 175.599997,601.339159 175.599997,602.228159 L175.599997,607.021159 C175.599997,607.573159 176.070397,608.000159 176.649997,608.000159 L181.127196,608.000159 C182.974146,608.000159 184.340196,609.642159 183.925446,611.355159" id="like-[#ffffff]"> </path>\\n </g>\\n </g>\\n </g>\\n </g>\\n</svg>';
/\x2f web/resources/ai.png
var ai_exports = {};
__export(ai_exports, {
default: () => ai_default
});
var ai_default = __toBinary("iVBORw0KGgoAAAANSUhEUgAAACcAAAAbCAYAAAD/G5bjAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAABIAAAASABGyWs+AAAJiklEQVRYw52XeXBV1R3HP+fe+/Yl+0JADBpACagQAwyM2AHtiEXrvtXaTseMY6fqUDvjtFJt1dEqgy1D1Tpu1bozCm4BF1CpGwYQZEvAsASSELK+Je++d9+799c/bnxJjNhOz8yZ97u/d37nfM9vP0pEONH4es1Lcs9Dq/l/xiFN/qd14+dNYNl1P6J+5uXqu/8ZJxL6aMuzsnDpcuSwz2UEbUjpoxcF7bGCIWsMSwW1sbyAFzEtblp8HfWn138vhu8F1/TVa7Kwc+UwsBKYWD2Fhpuuz68ZyDSTtSaOksulXBAD6mie52SiJO10/tuyB/J0PHEMI1AGKozkLFGGd5T2xoBr+uo1mbf9fiQggHK1o1lUlYZY1nAxCTNLLG0xoWBJXqYnYxE1PHg9OjbgODbpjEMk4BkG6QiiuWd/q/+83rODKMkC3lFYRum7vbtFLvYvJzcDVGj4EiqokRr0A7B+03b+cP/LoGn5efPvnuaNjdvyB+9rPc4F195Hwszm93h69SaWLV/DsGOYXHfH/TTv7wRPCJzMWBOKSH7efewOUfF6mdQxS/h4psAsoWSWMHuqTKy7UgZtR0RELDsnORkeg7Yjlj3MseycHOlLip0b5sVTlvT2J2XkOHCkR8x0xv1I98pILCIy2qzeUAwE5oyDg+1DmjNt9JoQbe+2suiWW4hGKvHbXXkZvzZu1GXnzDyNpVddyviCIEpLs+LFDzhwwKS4wN3PVAcJe/rx+EopjriGvWLJ9ZTh/+GAKPcFIA3dKAhLPhr1QsG5Eb6Ivw/NJnQGXYFxKagKuHRIQVOKth2L+O01l9H44S5m1ZayIfY86zq2Q2yEr+9PuXuMS8HkIAsuWkSZOu2HwU3qq4Qg7ESgzIAA0AvZiCDlDvrGCI9tC3Lu+TYFf17MXm0aDzU1ss5sgSr38NQXfmzgnU/3UFk6m8l1sK4oiL9YI9cOpX/XuWdHgHnLwyTOu4RnWlppO5aldmIIYCgKTxCtErTpTuhwMIcKZ5BeH1LuwCaHx7bZNNgKfhWF2b+mgnLOrqlj4RsNbAllkHF+BsxeMo7wyLKrAHhhzzgmVewDHJLLQ6zba3LW2SbceAFEbmfO6RYd/QexbAuvMtwgGwnOSfTIIyvvZeneT6AP1KEc0uYgKTfPqeMatZ8YNNgOlmbhZcisBAgXnsq9dZO4uXsvKqSRAwzbobHpG04ujjK3vIoWM8fxjV7qj6U46zvKUIaXytI4WaeF47sMCqtKxFtarvKp5OWv7+Lzn3+KbBNYD06zGlUNtCOKaY7jBo3jRf55EBL/wJYmYD2T2juZFs0xsRbMVAfxXJaejn4OHe5kWnQRNWe4pey8uPtrbTOQZ99DeAo4jJ58ndjW59liatTOuoJjx48JgLF16yfy1vzNlBwFp1pBN6hwBq3cj10i0DQ2/WTXF+G55jm0xe8hu9LEvvJw2vMaRG2aAxa5xAA3XDoXgN3WVmZ6FJ1T4YOooqHfvaB1WwbProeR6Y/CrjQp5wLK7i3gm9Qgl97wez5843Ex1nevpRvFeMO9Vdn1UHuFl1AgQ3WFl08edAHtXgRPrBEabIXX8UJjBdb6DKDYcJnOWZPBHHD3eKrxBaaMW0CoMEpZ/QaieDhvgbC8SrG9H85C3D0ed2PA0hSHVi3gzIoc9Su8JALb2bnnLfTxt5b9ace+AaZFFZ+9n6W2Vmfx1V6MSoPeQuHgEUH/Gm74q2LVq2ECgxlmqCy66OwUg39dbuB9Bsq9QtAPRWcLR61t7M+9SXvFGnyVu8miKPRDaI5i+WuKeSZUDlnB0iyeqK3izMfu4CQjRdcZq1HTBT2Ywkj3K9r+qBP/m4Me8rClV2iSLCqlI0GgQKhFMXmC0PDZII88qPHuliluNP20mdm/CVAStogPHTa9DvQ6LzYWkCU2VCFjQHUdzP8Mrr9Tp3aDuz55MVQ/HOZqx4tNiEYRNg9kaM0OuNEqCYeoT7nJdoRvhTVFrlij94DOIDaTJwhLV2VJ0koYB69chIcgg3YnWSeIR0thqn2j/NPEj0bUzYEcoqammOLVXcRtN+DKMIjqh+mydlNKHTNj49ms7aMilMEoN8IU3anhq7EBxaRJsBgdghBVQm9YeDKV43hS4QvbpHIOR7dN466z78ObqSaWtqgqCNKXy5BO24wrMunLuD1dsc9LX8ZCOcX4/DZpK+ZWHJ+DKBOlBdjifMRK5wF+ZOylljp+WTiXaqeFacqDNtVbQeZn4I/4KCoLct4MmKvbnGLalCZhoEfhdCs6ktDvBGgXxZfPllHCqfz7ixZuvO1RNE3x4aadLF32LDqlrGts4/Zl69CcEt5Yu5+7V7xJRIV45ZW9PP7cFgqd8bz0fBvvvRVjkZpKPJdht9Pnmj8Z4OMdGu9t1TFmBM4APgYgV5Pm06PChkbFQJ8ilhW0LQKmQ0fGIC0OcVHEA4dJZ3OE/Brd7d0ABHWdA82H3NQc1Gnd34Ya6t9a9x3Om7n90ACapuiK9dHeFefKi85hQIrZm/oGoiapeB/vrLK5pf5kjNmnL+H0nndIlbVSMCg0r9OwB7OQ8aD3KrLHM5Dy0XnEIXaSmS94lgmRcHDYP0PDXUXIGyCdMAEoKAySiCcBiEZCDFpuj1dRUMzuY+3olNJrT+Bt4ygQoLw/DKu9/OSSczGCoRL1ROdSeXLtQyRWdmMfTrqFIwU2Argl7HBWqJQwiZzQG+4nkT1IUVGpm5Qdm9LSIrc2O0JlaTgPtDwacgnHoSTiJdHv+l1FRZSPN7cAJiX6UXrtCdjASZVLWPuXCD9ecv5ws+lkMxLrz8iO11+U6SV1bqM5Ypa8M0vKxZ3qjpmyfs3bIrYtB470iNi2ZKyc7NrXISIiZjqTp+MpK0/39idd2rbzdM75Uqan60XF66U5HROxkpLp7pJRzaYyvCpaCKeds0hgxZiSFeuC3LdN/4Xwi40PMLVoLYHj3+n7P+gBoEQ7BTaO+GOILsrZ9Bs6FdkiLNXDQH8Le6akEDtEazDFVL0Sw+e6xPe+vtLBIPQOjuLZLW6rpVI6Mt+ma75Jd+LzUWuc5Mi36q5hMulwwhHWwHbdoEVe5UJuRdP83w8uF9CJXTaI3j0CWB/oUxU53H4vDybiGy0c4b+O8FAEV1saEUMR1G3KyFGRzTBFbQIaQO8Dwqjvvvizji3ze6+lPzH6cRyIjwbiN0ebM2D5Mb1pAtbYt4Av4iFQPKyHimKoMt1IrxSNIp9OYNAg5AkggQDVqpaayEL1H+TyrJ7zr7IrAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDEyLTAzLTA2VDE1OjAzOjM4LTA4OjAwYXQSEgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxMi0wMy0wNlQxNTowMzozOC0wODowMBApqq4AAABGdEVYdHNvZnR3YXJlAEltYWdlTWFnaWNrIDYuNi42LTIgMjAxMC0xMi0wMyBRMTYgaHR0cDovL3d3dy5pbWFnZW1hZ2ljay5vcmdAFj3CAAAAGHRFWHRUaHVtYjo6RG9jdW1lbnQ6OlBhZ2VzADGn/7svAAAAF3RFWHRUaHVtYjo6SW1hZ2U6OmhlaWdodAAyN8N0v5kAAAAWdEVYdFRodW1iOjpJbWFnZTo6V2lkdGgAMzlHieHxAAAAGXRFWHRUaHVtYjo6TWltZXR5cGUAaW1hZ2UvcG5nP7JWTgAAABd0RVh0VGh1bWI6Ok1UaW1lADEzMzEwNzUwMTjyXqMqAAAAEnRFWHRUaHVtYjo6U2l6ZQAyLjZLQkL1oUptAAAALXRFWHRUaHVtYjo6VVJJAGZpbGU6Ly8vdG1wL21pbmltYWdpY2sxNzcxNy0yNi5wbmfdD3aEAAAAAElFTkSuQmCC");
/\x2f web/resources/close-button.svg
var close_button_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="currentColor">\\n <path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/>\\n</svg>\\n';
/\x2f web/resources/favicon.png
var favicon_exports = {};
__export(favicon_exports, {
default: () => favicon_default
});
var favicon_default = __toBinary("iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAE7mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgOS4xLWMwMDIgNzkuNzhiNzYzOCwgMjAyNS8wMi8xMS0xOToxMDowOCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI2LjUgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAyNS0wNS0xOVQxNDozNDo1NiswODowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjUtMDUtMTlUMTQ6NDU6MDQrMDg6MDAiIHhtcDpNZXRhZGF0YURhdGU9IjIwMjUtMDUtMTlUMTQ6NDU6MDQrMDg6MDAiIGRjOmZvcm1hdD0iaW1hZ2UvcG5nIiBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjM3Y2IxMzY0LTBmY2UtNzI0NC04NTdmLTMyZjk5YzFhYTc0NSIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozN2NiMTM2NC0wZmNlLTcyNDQtODU3Zi0zMmY5OWMxYWE3NDUiIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDozN2NiMTM2NC0wZmNlLTcyNDQtODU3Zi0zMmY5OWMxYWE3NDUiPiA8eG1wTU06SGlzdG9yeT4gPHJkZjpTZXE+IDxyZGY6bGkgc3RFdnQ6YWN0aW9uPSJjcmVhdGVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjM3Y2IxMzY0LTBmY2UtNzI0NC04NTdmLTMyZjk5YzFhYTc0NSIgc3RFdnQ6d2hlbj0iMjAyNS0wNS0xOVQxNDozNDo1NiswODowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIDI2LjUgKFdpbmRvd3MpIi8+IDwvcmRmOlNlcT4gPC94bXBNTTpIaXN0b3J5PiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpA1ancAAAZaSURBVGiB1Zp/bJXVGcc/z3nvbbktxf6iP9jcKFSFgpMtC27gupnMxASdBjKTmUgiJtaxmMVhlrksy2ayLUuc/zAYzYJLZH8QIzNTms0fU0RhE/yFYnGFVhhdW8va0lrube+973n2x8ulLe/98b5413bf5P5x73tyzvf7Pec5z/Pec4Rs2JVYjutsQnQDSgtCNYjJ2vZ/BrXACEonyH4cdx8PxLovbyUzvrVrGenUZuB7oCsRcWaJbX6ouoh8iLAdE91Dm8Qzj6YE7NQqSP4S5T5EonNCtBBUU8ATSMkjbJURyAho1zLSyccRaZtLfsGhu3BKttEmcW9d29Q9wJa5JRUCyn0XlzrCrsRyrPNnYNUc0woH5TiOe6fBdTahumKu+YSHrsR1NkVAb5s3u00YiDigGyIILcXs1xFYtkgodWBoAvrjmrVdqYGqBUJlqbeTjCZheEKZcEMMprREgKpiEM9gfaPwh5sdKqLCR58o977s0jniiSh1YF2DcOvVhrV1wmcWQnkERCCegr44vH1Oef6s5UCfkkgXGEyoFnYms1uUA3kdVrir2bD3Fge5mGH2dFkePuyyqlr4wQ0OX18iVBTIMok0HBpQtr9v6ThjcfMwjIQhD/kdjhhQwAKZoNrYZCiPwE2NhrpYsDFiEfjmZ4Wv1Du0dwo/P+rySSqggEION8SEpgpBBBbHhB99yVxyuKlCfAOVR2HjsisroxZGYdsNhiXlsPWgy/nJAAI+jcN7/ml5YzDUigyE7zQbxpLw4GsuKZtPQA6H7z/g8tWG4jrsKvRfUPrjnin1MVhSLkRzdHXvdYa3zim/75ypwDcDs+Hwm+eU9g8sB/uUtfVetB8eUNY3CG0thnWNclmZDCUOfP96w1/\x2fpZwdn+JQ0LqMw0EDsBCe6la2vOKyolK4v8WwplZYU+sR7zqv3PGXNLtPWGwWn1ZVC99ePlNa6F3o0+DVPuVXb7tsvtawbY3fuw2fF256xuWhQy61C+DOJn+b25cadh63lxLerL1lxdPw+DFL14hSWUpOh7esFMaT8Ni7lqEJf5uWKuGaq6ZmoagCukdzx8fxYSVtFUV46JDLs6dt1na3LzVEHDgyqBzNEm+VpbBsEV6wUkQB8TQc7M8t4NSosr7R8N3VhR1eWSmkLHww4u8vaqA2doUzUMjhU3meJ9JeLRTG4UTa/1zw8lFoAUEcTuapJOtiMJhQXu3TwA43ll2+mXqxE5+WiwILCOJwvgxxfY3wpx4l5hR2uHNYqYnBlxf7BSTSeHlAQgoI4rDxj3cJSyuEu68RjIH+hPdbNocfe9eSdOGu5YbVNf7nfXGla3Tqe2ABQRyuLs2jANi62uHAv5WPL0BZ1O/wb45ZUi7EovDwGoOTpbvDA0r/hSkmgRNZEIe/uDh/H/Ux+F2r4dE3LYcH4IVe5aXeKTI9Y8rQJOz+hsOyRf7BLqThqVN2hpGBBQRxeF2DuVS55sLVC4UdrQ7/+Fh54azl5KgXmE0V8K0mw9cahbIcrPaftrzYO7P/wAKCOFzoTSuDEgOtjUJro4NVb2lmWy7T0TOm/Pod6yunQ+WBdQ3FrzyMFCY/mIAf/t3yzn/8sxuKUVCHi4nece9FZl9P9tJj1qrRsaSXpGIBR0xbeKVPefSoy+sDueNq1gR0nLE885Gy+TrDjfVCTal/V1Pg/KT318ofuyxP91jGc7zMZ1A0AWNJfAE2HeMpuLFO+Fuv5adHlC/UCM1XCTULvHU8kvRqrfeGvE/QP7iKJqDjjGXvScu1VdkjUgRUoGaBsLbOc3nvScvkdNHK5UcuBRFYQBCHP7dQsJp9V1GFp7stquR2OCR5CCEgqMPvDSu/eMtSPm3HUuC505Y3BvSKSOZDYAHjKTg2pDRXSk6Hz00ok2n42VGXLIVm0ckDRLzTwMInkCIegSODs+twfqiNoAwj1GZ+yreGwavFf3IkxxYxq+QBGIkgdAKtGQL5HJ53UDojqHSArs+c0uR1eD5B1QXZb3DcfSAn5ppPaIh8iOPuMxeP73dcPET+/4BqCmE7D8S6vd0nEn0SYfcc0wqDJzDRPZApp9skDiU/RrV9Xs+Eagp0F1LySOa+hP+yh03dg/IgqivmzfGrF7AngB1Eok9mv+wxHb9NNCPORu8MmRagak6u23g5qhOVjlzXbf4LqdDarULl/3EAAAAASUVORK5CYII=");
/\x2f web/resources/page-icon-hover.png
var page_icon_hover_exports = {};
__export(page_icon_hover_exports, {
default: () => page_icon_hover_default
});
var page_icon_hover_default = __toBinary("iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAGu2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDUgNzkuMTYzNDk5LCAyMDE4LzA4LzEzLTE2OjQwOjIyICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ0MgKFdpbmRvd3MpIiB4bXA6Q3JlYXRlRGF0ZT0iMjAxOC0wNi0zMFQwMjowMToxNy0wNTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMC0wMS0xMlQxODo1NDowNi0wNjowMCIgeG1wOk1vZGlmeURhdGU9IjIwMjAtMDEtMTJUMTg6NTQ6MDYtMDY6MDAiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6YTk5ZGEzMWUtNTM5Zi03OTQ3LWI5MzMtNzRiYzliZDMyYjRmIiB4bXBNTTpEb2N1bWVudElEPSJhZG9iZTpkb2NpZDpwaG90b3Nob3A6MjE2NDcxMGYtOTM5My1hZjRmLTg3ZWUtY2NmY2YxNGIzMjYzIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6ZTAzMzRhNzEtMmQ2Mi1lNDRjLWFiMjUtZGJjZTNlYTcwNTYwIiBkYzpmb3JtYXQ9ImltYWdlL3BuZyIgcGhvdG9zaG9wOkNvbG9yTW9kZT0iMyIgcGhvdG9zaG9wOklDQ1Byb2ZpbGU9InNSR0IgSUVDNjE5NjYtMi4xIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDplMDMzNGE3MS0yZDYyLWU0NGMtYWIyNS1kYmNlM2VhNzA1NjAiIHN0RXZ0OndoZW49IjIwMTgtMDYtMzBUMDI6MDE6MTctMDU6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCBDQyAoV2luZG93cykiLz4gPHJkZjpsaSBzdEV2dDphY3Rpb249InNhdmVkIiBzdEV2dDppbnN0YW5jZUlEPSJ4bXAuaWlkOjYxNjM4NWIxLTA4YjUtZDI0YS1iNDMyLTcwM2IwZjQxYzYxOSIgc3RFdnQ6d2hlbj0iMjAxOC0wNi0zMFQwMjowMToxNy0wNTowMCIgc3RFdnQ6c29mdHdhcmVBZ2VudD0iQWRvYmUgUGhvdG9zaG9wIENDIChXaW5kb3dzKSIgc3RFdnQ6Y2hhbmdlZD0iLyIvPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0ic2F2ZWQiIHN0RXZ0Omluc3RhbmNlSUQ9InhtcC5paWQ6YTk5ZGEzMWUtNTM5Zi03OTQ3LWI5MzMtNzRiYzliZDMyYjRmIiBzdEV2dDp3aGVuPSIyMDIwLTAxLTEyVDE4OjU0OjA2LTA2OjAwIiBzdEV2dDpzb2Z0d2FyZUFnZW50PSJBZG9iZSBQaG90b3Nob3AgQ0MgMjAxOSAoV2luZG93cykiIHN0RXZ0OmNoYW5nZWQ9Ii8iLz4gPC9yZGY6U2VxPiA8L3htcE1NOkhpc3Rvcnk+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+iRnHuAAAAEFJREFUOI1j/P/\x2fPwMlgIki3QwMDCxIbEJOYaSJC0g14D8Dmkvp7oLhaAByOsAaz6QYAAMkpW2qegEGSPLKwMcCANSMByRCC4aMAAAAAElFTkSuQmCC");
/\x2f web/resources/index.html
var resources_default = \`<!doctype html>
<!-- This is used by VView as the default top-level page. PPixiv doesn't use this. -->
<head>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="vview">
<meta name="viewport" content="viewport-fit=cover, initial-scale=1, user-scalable=no">
<link rel="apple-touch-icon" href="/resources/vview-icon.png">
<link rel="manifest" href="/resources/manifest.json">
<!-- This is displayed until we're loaded enough to set our own title. Set something,
so the browser doesn't just show the URL. -->
<title>VView</title>
<!-- This style is temporary and is just here to avoid a #FFFFFF flashbang from the background
before we load the stylesheet. It matches the dark grey set by the manifest. This style is
removed by main_controller once we have the real stylesheet. -->
<style id="initial-style">
body { background-color: #222; }
</style>
<!-- In VVbrowser, leave it on the default background. It has a dark background which will
always be displayed before we get here, and if we're displaying an image it's better to leave
it on black the whole time than to flash #222 briefly. -->
<script>
(() => {
let in_vvbrowser = navigator.userAgent.indexOf("VVbrowser/") != -1;
if(in_vvbrowser)
document.querySelector("#initial-style").disabled = true;
})();
<\\/script>
<script src="/vview/app-bundle.js"><\\/script>
\`;
/\x2f web/resources/zoom-plus.svg
var zoom_plus_default = '<svg xmlns="http:/\x2fwww.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" stroke-width="2" stroke="currentColor">\\n <circle cx="16" cy="16" r="10" fill="none" />\\n <line x1="12" y1="16" x2="20" y2="16" />\\n <line x1="16" y1="12" x2="16" y2="20" />\\n</svg>\\n';
/\x2f web/resources/auth.html
var auth_default = \`<!doctype html>
<head>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="vview">
<meta name="viewport" content="viewport-fit=cover, initial-scale=1.2, user-scalable=no">
<link rel="apple-touch-icon" href="/resources/vview-icon.png">
<link rel="manifest" href="/resources/manifest.json">
<body>
<!--
This is just a trivial UI to handle logging in. We don't have access to any
other files right now, so this is all baked into this one file.
-->
<style>
html {
height: 100%;
margin: 0 0;
}
body {
font-family: "Helvetica Neue", arial, sans-serif;
background-color: #222;
color: #fff;
font-size: 200%;
height: 100%;
margin: 0;
}
.content {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-items: center;
}
#login-form {
display: flex;
max-width: 20em;
flex-direction: column;
gap: 0.5em;
align-items: center;
}
input::placeholder {
color: #111;
}
input {
font-size: unset;
padding: 0.25em 0.5em;
}
input#submit {
padding: 0.25em 2em;
min-width: 75%;
}
#error {
margin: 1em 0;
}
</style>
<div class=content>
<div style="flex: 1;"></div>
<form id=login-form>
<input id=username type=text placeholder=Username required autofocus autocapitalize=off>
<input id=password type=password placeholder=Password required autocomplete=current-password>
<input type=submit id=submit value=Login>
</form>
<div style="flex: 1;"></div>
<div id=error hidden></div>
</div>
<script>
async function post(path, data={})
{
let response = await fetch(path, {
method: 'POST',
body: JSON.stringify(data, 4),
});
return await response.json();
}
/\x2f Check if we're already logged in (as a non-guest user).
async function checkLogin()
{
let result = await post("/api/info");
if(result.success && result.username != "guest")
{
console.log("Already logged in");
redirectBack();
}
}
function redirectBack()
{
let query = new URLSearchParams(document.location.search);
let url = query.get('url');
let state = history.state;
if(url == null)
{
console.error("Return URL is missing");
url = new URL("/", document.location);
state = null;
}
/\x2f Replace this history entry to go back to the original page. If the page
/\x2f had history state it preserved it when it redirected here, and we'll preserve
/\x2f it when going back.
history.replaceState(state, "", url);
document.location.reload();
}
/\x2f When the window gains focus, check if we were logged in in another tab.
window.addEventListener("focus", async(e) => { await checkLogin(); });
async function login(username, password)
{
let result = await post("/api/auth/login", {
username, password
});
if(!result.success)
{
setMessage(result.reason);
return;
}
/\x2f Save the token. If we're on HTTPS, set it as a secure token.
let token = result.token;
let secure = document.location.protocol == "https:"? "; Secure; SameSite=Strict":"";
document.cookie = \\\`auth_token=\\\${token}; max-age=99999999; path=/\\\${secure}\\\`;
/\x2f Redirect back to the original URL.
redirectBack();
}
function setMessage(text)
{
let error = document.querySelector("#error");
error.innerText = text;
error.hidden = text == null;
}
async function setup()
{
let inputUsername = document.querySelector("#username");
let inputPassword = document.querySelector("#password");
document.querySelector("#submit").addEventListener("click", (e) => {
e.preventDefault();
login(inputUsername.value, inputPassword.value);
});
await checkLogin();
}
setup();
<\\/script>
\`;
/\x2f output/intermediate/css/main.scss
var main_default = \`* {
overscroll-behavior: contain;
touch-action: pan-x pan-y;
-webkit-tap-highlight-color: rgba(255, 255, 255, 0);
-webkit-touch-callout: none;
}
html {
overflow: hidden;
width: 100%;
height: 100%;
touch-action: none !important;
position: relative;
--fixed-safe-area-inset-bottom: env(safe-area-inset-bottom);
--fullscreen-left: 0;
--fullscreen-top: 0;
--fullscreen-right: 0;
--fullscreen-bottom: 0;
--device-edge-radius: 0;
}
html.ios {
height: calc(100% + env(safe-area-inset-top));
}
html.ios {
--fixed-safe-area-inset-bottom: calc(env(safe-area-inset-bottom) * 0.5);
}
html[data-display-mode=notch][data-orientation="0"] {
--fullscreen-top: env(safe-area-inset-top);
}
html[data-display-mode=notch][data-orientation="90"] {
--fullscreen-left: env(safe-area-inset-left);
}
html[data-display-mode=notch][data-orientation="-90"] {
--fullscreen-right: env(safe-area-inset-right);
}
html[data-display-mode=notch][data-orientation="180"] {
--fullscreen-bottom: env(safe-area-inset-bottom);
}
html[data-display-mode=safe] {
--fullscreen-top: env(safe-area-inset-top);
}
html[data-display-mode=notch] {
--device-edge-radius: 14vmin;
}
html:not(.macos) .search-results::-webkit-scrollbar {
width: 10px;
background-color: rgb(30, 30, 30);
}
html:not(.macos) ::-webkit-scrollbar {
width: 8px;
background-color: #888;
}
html:not(.macos) ::-webkit-scrollbar:hover {
background-color: #888;
}
html:not(.macos) ::-webkit-scrollbar-thumb {
transition: background-color 0.25s;
background-color: rgb(0, 0, 0);
border: 1px solid rgba(0, 0, 0, 0);
background-clip: padding-box;
}
html:not(.macos) ::-webkit-scrollbar-thumb:hover {
background-color: #008;
}
html:not(.macos) ::-webkit-resizer {
background-color: #222;
}
.ppixiv-icon {
font-family: "ppixiv";
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
letter-spacing: 0;
font-feature-settings: "liga";
-webkit-font-feature-settings: "liga";
font-variant-ligatures: discretionary-ligatures;
-webkit-font-variant-ligatures: discretionary-ligatures;
-webkit-font-smoothing: antialiased;
}
@font-face {
font-family: "Material Icons";
font-style: normal;
font-weight: 400;
src: url("https:/\x2ffonts.gstatic.com/s/materialicons/v129/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2") format("woff2");
}
.material-icons {
font-family: "Material Icons";
font-weight: normal;
font-style: normal;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
}
body {
font-family: "Figtree", sans-serif;
margin: 0;
color: #fff;
height: 100%;
}
a {
text-decoration: none;
color: inherit;
}
html.firefox [inert] {
pointer-events: none !important;
}
html.firefox [inert] * {
pointer-events: none !important;
}
html.mobile * {
user-select: none;
-webkit-user-select: none;
}
html.mobile .popup:after {
display: none !important;
}
html.mobile > body {
font-size: 150%;
}
/* Theme colors: */
html {
--icon-size: 1.7;
--icon-row-gap: 0.5em;
--border-radius-large: 8px;
--border-radius-small: 4px;
--main-background-color: #222;
--background-noise: var(--dark-noise);
--button-highlight-color: #eee;
--button-disabled-color: #444;
--input-outline: #444;
--input-outline-focused: #888;
/* Colors for major UI boxes */
--ui-bg-color: rgba(0, 0, 0, 0.6);
--ui-fg-color: #fff;
--ui-border-color: #63636380;
--ui-bg-section-color: #3b3b3b;
/* color for sections within UI, like the description box */
--ui-bg-blur: blur(20px) brightness(90%) saturate(112%);
/* Color for frames like popup menus */
--frame-bg-color: rgba(0, 0, 0, 0.6);
--frame-fg-color: #fff;
--frame-border-color: #444;
--dropdown-menu-hover-color: #444;
--button-active-bgcolor: #7c7c7c !important;
--button-bgcolor: #5c5c5c !important;
--button-color: #fff !important;
--button-hover-bgcolor: #6e6e6e !important;
--button-primary-active-bgcolor: #3289cb !important;
--button-primary-bgcolor: #006cbe !important;
--button-primary-color: #fff !important;
--button-primary-hover-bgcolor: #1c7cc5 !important;
/* Box links used for selection in the search UI: */
--box-link-fg-color: var(--frame-fg-color);
--box-link-bg-color: var(--button-bgcolor);
--box-link-disabled-color: GrayText;
--box-link-hover-color: var(--button-hover-bgcolor);
--box-link-selected-color: var(--button-active-bgcolor);
--box-link-selected-hover-color: var(--button-hover-bgcolor);
/* Color for the minor text style, eg. the bookmark and like counts.
* This is smaller text, with a text border applied to make it readable. */
--minor-text-fg-color: #aaa;
--minor-text-shadow-color: #000;
--title-fg-color: #fff;
/* title strip in image-ui */
--title-bg-color: #444;
--like-button-color: #888;
--like-button-liked-color: #ccc;
--like-button-hover-color: #fff;
--context-menu-bg-color: var(--button-bgcolor);
}
html.mobile {
--icon-size: 3;
--icon-row-gap: 0.5em;
--main-background-color: #000;
--background-noise: none;
}
html.mobile [data-hidden-on~=mobile] {
display: none !important;
}
html.ios [data-hidden-on~=ios] {
display: none !important;
}
html.android [data-hidden-on~=android] {
display: none !important;
}
[hidden] {
display: none !important;
}
vv-container {
display: contents;
}
input,
textarea,
[contenteditable] {
padding: 1px 2px;
font-family: unset;
font-size: unset;
background-color: var(--frame-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--frame-fg-color);
border: none;
outline: 1px solid var(--input-outline);
}
input:focus-within,
textarea:focus-within,
[contenteditable]:focus-within {
outline: 1px solid var(--input-outline-focused);
}
/* Pixiv sometimes displays a random Recaptcha icon in the corner. It's hard to prevent this since it
* sometimes loads before we have a chance to stop it. Try to hide it. */
.grecaptcha-badge {
display: none !important;
}
.viewer-images {
touch-action: none;
}
.viewer-images .image-box {
position: relative;
transform-origin: 0 0;
right: auto;
bottom: auto;
}
.viewer-images .image-box > .crop-box {
position: relative;
width: 100%;
height: 100%;
}
.viewer-images .image-box.cropping {
overflow: hidden;
}
html.ios .viewer-images .image-box.cropping {
will-change: transform;
}
html:not(.ios) .viewer-images > .image-box img {
will-change: transform;
}
.viewer-images .displayed-image {
position: absolute;
width: 100%;
height: 100%;
}
.viewer-images .displayed-image:not(.main-image) {
pointer-events: none;
}
.viewer-images .inpaint-image,
.viewer-images .low-res-preview {
pointer-events: none;
}
.viewer-video > .video-container {
width: 100%;
height: 100%;
touch-action: none;
}
.viewer-video .top-seek-bar {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 10px;
pointer-events: none;
}
.viewer-video .top-seek-bar .seek-bar {
height: 4px;
}
.viewer-video .top-seek-bar .seek-bar > .seek-parts {
transition: transform 0.25s;
transform: scale(100%, 0%);
transform-origin: top;
}
.viewer-video .top-seek-bar .seek-bar.dragging > .seek-parts {
transform: scale(100%, 100%) !important;
}
.checkbox {
font-size: 150%;
}
.video-ui {
position: absolute;
bottom: 0;
left: 0px;
width: 100%;
user-select: none;
-webkit-user-select: none;
touch-action: none;
opacity: 0;
}
html.mobile .video-ui {
bottom: 0;
}
html:not(.mobile) .video-ui {
transition: transform 0.25s, opacity 0.25s;
}
.mouse-hidden-box.cursor-active html:not(.mobile) .video-ui, html:not(.mobile) .video-ui.dragging, html:not(.mobile) .video-ui:hover, html:not(.mobile) .video-ui.show-ui {
opacity: 1;
}
.mouse-hidden-box.cursor-active html:not(.mobile) .video-ui .seek-bar[data-position=top] > .seek-parts, html:not(.mobile) .video-ui.dragging .seek-bar[data-position=top] > .seek-parts, html:not(.mobile) .video-ui:hover .seek-bar[data-position=top] > .seek-parts, html:not(.mobile) .video-ui.show-ui .seek-bar[data-position=top] > .seek-parts {
transform: scale(100%, 50%);
}
html.mobile .video-ui {
opacity: var(--menu-bar-pos);
}
html.mobile .video-ui .seek-bar[data-position=top] > .seek-parts {
transform: scale(100%, calc(50% * var(--menu-bar-pos)));
}
html.mobile .video-ui:not(.show-ui) {
pointer-events: none;
}
.video-ui .seek-bar {
height: 12px;
}
.video-ui .seek-bar[data-position=top] {
padding-top: 25px;
}
html.mobile .video-ui .seek-bar {
padding-top: 2em;
}
.video-ui .seek-bar[data-position=top] > .seek-parts {
transition: transform 0.25s;
transform: scale(100%, 0%);
transform-origin: bottom;
}
.video-ui .seek-bar[data-position=bottom] {
height: 4px;
}
.video-ui .seek-bar[data-position=bottom] > .seek-parts > [data-seek-part=empty] {
background-color: rgba(0, 0, 0, 0.5);
}
.video-ui .seek-bar.dragging > .seek-parts {
transform: scale(100%, 100%) !important;
}
.video-ui > .video-ui-strip {
width: 100%;
padding: 0.25em 1em;
display: flex;
flex-direction: row;
color: #ffffff;
align-items: center;
gap: 10px;
background-color: rgba(0, 0, 0, 0.5);
cursor: default;
padding-top: 4px;
}
.video-ui > .video-ui-strip .button {
cursor: pointer;
}
.video-ui > .video-ui-strip .font-icon {
font-size: 36px;
}
.video-ui > .video-ui-strip > .time {
font-family: Roboto, Arial, Helvetica, sans-serif;
font-size: 1.2em;
}
.video-ui > .video-ui-strip .volume-slider {
display: flex;
flex-direction: row;
align-items: center;
margin-right: -10px;
padding: 0.5rem 0;
cursor: pointer;
}
.video-ui > .video-ui-strip .volume-slider > .volume-line {
height: 0.25rem;
width: 100px;
}
.seek-bar {
width: 100%;
box-sizing: content-box;
cursor: pointer;
position: relative;
}
.seek-bar > .seek-parts {
width: 100%;
height: 100%;
}
.seek-bar > .seek-parts > [data-seek-part] {
height: 100%;
position: absolute;
left: 0;
top: 0;
}
.seek-bar > .seek-parts > [data-seek-part=fill] {
background-color: #f00;
}
.seek-bar > .seek-parts > [data-seek-part=loaded] {
background-color: #a00;
}
.seek-bar > .seek-parts > [data-seek-part=empty] {
background-color: rgba(0, 0, 0, 0.25);
width: 100%;
}
.title-font {
font-weight: 700;
font-size: 20px;
font-family: system-ui, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Droid Sans, Helvetica Neue, Hiragino Kaku Gothic ProN, Meiryo, sans-serif;
}
.small-font {
font-size: 0.8em;
}
.hover-message {
display: flex;
justify-content: center;
align-items: center;
user-select: none;
-webkit-user-select: none;
margin: 0;
color: var(--frame-fg-color);
font-size: 1em;
pointer-events: none;
font-size: 1.5rem;
position: fixed;
left: max(env(safe-area-inset-left), 1rem);
right: max(env(safe-area-inset-right), 1rem);
--extra-distance: 0px;
bottom: calc(2vh + env(safe-area-inset-bottom) + var(--extra-distance));
z-index: 100000;
/* over everything */
transition: opacity 0.25s, bottom 0.25s;
opacity: 0;
}
.hover-message > .message {
background-color: rgba(0, 0, 0, 0.5);
border-radius: var(--border-radius-large);
overflow: hidden;
text-align: center;
max-width: 40em;
max-height: 50vh;
padding: 0.25em 0.5em;
}
html.mobile.illust-menu-visible .hover-message {
--extra-distance: 3em;
}
.hover-message.show {
opacity: 1;
}
.image-ui .disabled {
display: none;
}
.image-ui .count {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
mix-blend-mode: difference;
}
.image-ui .hover-sphere {
width: 500px;
height: 500px;
/* Clamp the sphere to a percentage of the viewport width, so it gets smaller for
* small windows. */
max-width: 30vw;
max-height: 30vw;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
.image-ui .hover-sphere circle {
pointer-events: auto;
/* reenable pointer events that are disabled on .ui */
}
.image-ui .hover-sphere > svg {
width: 100%;
height: 100%;
transform: translate(-30%);
}
.image-ui .ui-box {
padding: 8px;
backdrop-filter: var(--ui-bg-blur);
position: absolute;
top: 1vmin;
left: 1vmin;
min-width: min(450px, 90vw);
max-height: min(500px, 100vh - 2vmin);
width: 30%;
color: var(--ui-fg-color);
border: solid 1px var(--ui-border-color);
background-color: var(--ui-bg-color);
border-radius: var(--border-radius-large);
transition: transform 0.25s, opacity 0.25s;
overflow-x: hidden;
overflow-y: overlay;
opacity: 1;
transform: translate(0, 0);
}
.image-ui .ui-box .button-row {
width: 70%;
margin-left: 4px;
padding-block-end: 8px;
}
body:not(.force-ui) .image-ui .ui-box.ui-hidden {
opacity: 0;
transform: translate(-50px, 0);
pointer-events: none;
user-select: none;
}
.image-ui .ui-box .avatar-popup {
position: absolute;
width: 4em;
top: 8px;
right: 8px;
}
.image-ui .ui-box .ui-title-box {
display: flex;
flex-direction: row;
margin-right: 4em;
}
.image-ui .ui-box .post-info {
padding-inline: 8px;
padding-block-end: 8px;
}
.image-ui .ui-box .post-info > * {
display: inline-block;
background-color: var(--box-link-bg-color);
color: var(--box-link-fg-color);
border-radius: var(--border-radius-large);
font-size: 0.8em;
font-weight: bold;
padding: 8px;
}
.image-ui .ui-box .post-info > *:hover {
background-color: var(--box-link-hover-color);
}
.image-ui .ui-box .description {
white-space: pre-wrap;
padding-inline: 8px;
padding-block: 8px;
overflow-wrap: break-word;
border-top: solid 1px var(--ui-border-color);
}
.image-ui .ui-box .author {
vertical-align: top;
}
.title-with-button-row-container {
padding-inline: 1em;
margin: auto;
}
.title-with-button-row-container .title-with-button-row {
display: flex;
flex-direction: row;
align-items: start;
}
@media (hover: hover) {
.title-with-button-row-container {
height: 2em;
overflow: hidden;
}
.title-with-button-row-container:hover {
overflow: visible;
}
.title-with-button-row-container:hover .title-with-button-row {
background-color: var(--ui-bg-color);
position: relative;
z-index: 1;
}
}
.image-info-hover-dock {
max-width: 70%;
min-height: 3em;
padding-block-end: 16px;
padding-inline: 8px;
}
.button-row {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
overflow: auto;
align-items: center;
gap: var(--icon-row-gap);
}
.button-row .button.enabled {
cursor: pointer;
}
.icon-button {
color: var(--button-color);
cursor: pointer;
user-select: none;
-webkit-user-select: none;
display: block;
width: calc(1em * var(--icon-size));
height: calc(1em * var(--icon-size));
flex-shrink: 0;
}
.icon-button .material-icons,
.icon-button .ppixiv-icon {
display: block;
font-size: calc(100% * var(--icon-size));
}
.icon-button svg {
display: block;
width: 100%;
height: 100%;
}
@media (hover: hover) {
.icon-button:hover {
color: var(--button-highlight-color);
}
}
.icon-button.highlighted {
color: var(--button-highlight-color);
}
.popup-visible > .icon-button {
color: var(--button-highlight-color);
}
.icon-button.disabled {
color: var(--button-disabled-color);
}
@media (hover: hover) {
.disable-ui-button:hover {
color: #0096fa;
}
}
.avatar-widget {
display: block;
position: relative;
}
.avatar-widget .avatar {
transition: filter 0.25s;
display: block;
border-radius: var(--border-radius-large);
object-fit: cover;
aspect-ratio: 1;
width: 100%;
height: 100%;
}
@media (hover: hover) {
.avatar-widget .avatar:hover {
filter: contrast(1.3);
}
}
.avatar-widget .follow-icon {
position: absolute;
bottom: 5%;
right: 5%;
width: 50%;
/* half the size of the container */
max-width: 2em;
/* limit the size for larger avatar displays */
pointer-events: none;
}
.avatar-widget .follow-icon > svg {
display: block;
width: 100%;
height: auto;
transition: opacity 0.25s;
/* Move the icon down, so the bottom of the eye is along the bottom of the
* container and the lock (if visible) overlaps. */
margin-bottom: -20%;
}
.avatar-widget .follow-icon > svg .middle {
transition: transform 0.1s ease-in-out;
transform: translate(0px, -2px);
}
.follow-widget {
max-width: min(90vw, 40em);
}
.follow-widget .separator {
height: 2px;
width: 100%;
background-color: #fff;
margin: 2px 0;
}
.follow-widget .material-icons {
margin-right: 8px;
}
.title-block {
display: inline-block;
padding: 8px;
color: var(--title-fg-color);
margin-right: 1em;
}
@media (hover: hover) {
.title-block.popup:hover:after {
top: 40px;
bottom: auto;
}
}
/* When .dot is set, show images with nearest neighbor filtering. */
body.dot img.filtering,
body.dot canvas.filtering {
image-rendering: pixelated;
}
/* Override obnoxious colors in descriptions. Why would you allow this? */
.description * {
color: var(--ui-fg-color);
}
.popup {
position: relative;
}
@media (hover: hover) {
.popup:hover:after {
pointer-events: none;
background: #111;
border-radius: 0.5em;
top: -2em;
color: #fff;
content: attr(data-popup);
display: block;
padding: 0.3em 1em;
position: absolute;
text-shadow: 0 1px 0 #000;
white-space: nowrap;
z-index: 98;
}
.popup[data-popup-side=left]:hover:after {
right: 0em;
}
.popup[data-popup-side=right]:hover:after, .popup:not([data-popup-side]):hover:after {
left: 0em;
}
.popup.popup-bottom:hover:after {
top: auto;
bottom: -2em;
}
}
body:not(.premium) .premium-only {
display: none;
}
body:not(.native) .native-only {
display: none;
}
body:not(.pixiv) .pixiv-only {
display: none;
}
body.hide-r18 .r18 {
display: none;
}
body.hide-r18g .r18g {
display: none;
}
.dropdown-box {
position: absolute;
overflow-x: hidden;
overflow-y: auto;
border: 1px solid var(--frame-border-color);
background-color: var(--frame-bg-color);
border-radius: var(--border-radius-large);
backdrop-filter: var(--ui-bg-blur);
}
.popup-menu-box {
overflow-y: auto;
min-width: 10em;
padding: 0.25em 0.25em;
}
.popup-menu-box.hover-menu-box {
visibility: hidden;
}
.popup-visible .popup-menu-box.hover-menu-box {
visibility: inherit;
}
/* This is an invisible block underneath the hover zone to keep the hover UI visible. */
.hover-area {
position: absolute;
top: -50%;
left: -33%;
width: 150%;
height: 200%;
z-index: -1;
}
.screen-search-container {
--nav-bar-reserved-height: 1em;
--title-height: 0px;
}
.screen-search-container .search-ui-mobile {
position: fixed;
top: 0;
translate: 0 calc(var(--title-height) * -1);
transition: translate 0.25s, opacity 0.25s;
opacity: 0;
font-size: 1.2rem;
width: 100%;
z-index: 1;
padding: 0.5em;
padding-top: max(0.5em, env(safe-area-inset-top));
backdrop-filter: blur(15px) contrast(60%) brightness(0.5);
-webkit-backdrop-filter: blur(15px) contrast(60%) brightness(0.5);
}
.screen-search-container .search-ui-mobile.shown {
translate: 0px 0px;
opacity: 1;
}
.screen-search-container .search-ui-mobile .search-title {
text-align: center;
display: block;
max-height: 10vh;
overflow-y: auto;
}
.screen-search-container .search-ui-mobile .avatar-widget {
width: 100px;
}
.screen-search-container .search-ui-mobile .data-source-ui {
margin: auto !important;
padding-inline: 8px;
}
.screen-search-container .search-ui-mobile .data-source-ui .box,
.screen-search-container .search-ui-mobile .data-source-ui -button-row,
.screen-search-container .search-ui-mobile .data-source-ui .box-button-row-group {
justify-content: center;
}
.screen-search-container .search-title .word {
padding: 0px 5px;
vertical-align: middle;
line-height: 1.5em;
}
.screen-search-container .search-title .word.paren {
font-weight: 400;
}
.screen-search-container .search-title .word:first-child {
padding-left: 0px;
/* remove left padding from the first item */
}
.screen-search-container .search-title .word.or {
font-size: 60%;
padding: 0;
color: #bbb;
}
.screen-search-container .mobile-navigation-bar {
backdrop-filter: blur(15px) contrast(60%) brightness(0.5);
-webkit-backdrop-filter: blur(15px) contrast(60%) brightness(0.5);
position: fixed;
left: 0;
width: 100%;
z-index: 1;
display: flex;
flex-direction: column;
--icon-size: 2;
--button-color: #5a91f7;
--button-disabled-color: #777;
font-size: 1.2rem;
transition: bottom 0.25s;
bottom: calc(var(--nav-bar-height) * -1);
}
html.mobile .screen-search-container .mobile-navigation-bar {
padding-top: 0px;
padding-bottom: env(safe-area-inset-bottom);
}
html.mobile:not([data-has-bottom-inset]) .screen-search-container .mobile-navigation-bar {
padding-top: 0.75rem;
padding-bottom: 0.75rem;
}
html.mobile[data-has-bottom-inset] .screen-search-container .mobile-navigation-bar {
padding-top: 0.5rem;
padding-bottom: calc(env(safe-area-inset-bottom) * 0.5);
}
.screen-search-container .mobile-navigation-bar.shown {
bottom: -1px;
}
.screen-search-container .mobile-navigation-bar:not(.shown) {
pointer-events: none;
}
.screen-search-container .mobile-navigation-bar .header-contents {
display: flex;
flex-direction: row;
justify-content: space-around;
}
.screen-search-container .search-results {
margin-left: var(--navigation-box-reserved-width);
height: 100%;
padding-top: calc(env(safe-area-inset-top) + var(--title-height));
padding-bottom: calc(env(safe-area-inset-bottom));
}
.screen-search-container .search-desktop-ui {
position: sticky;
width: 100%;
display: flex;
flex-direction: row;
justify-content: center;
padding-top: max(1em, env(safe-area-inset-top));
padding-bottom: 1em;
z-index: 1;
transition: top ease-out 0.2s;
}
html:not(.mobile) .screen-search-container .search-desktop-ui {
pointer-events: none;
}
html:not(.mobile) .screen-search-container .search-desktop-ui .thumbnail-ui-box {
pointer-events: auto;
}
.screen-search-container .search-desktop-ui:not(.ui-on-hover) {
top: 0;
}
.screen-search-container .search-desktop-ui.ui-on-hover {
top: 0;
}
html:not(.mobile) .screen-search-container .search-desktop-ui.ui-on-hover {
pointer-events: auto;
}
.screen-search-container .search-desktop-ui.ui-on-hover:not(.hover):not(.force-open) {
top: calc(-1 * var(--ui-box-height) + 40px);
}
.screen-search-container .thumbnail-ui-box {
display: flex;
padding: 6px;
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--ui-fg-color);
border-radius: var(--border-radius-large);
box-shadow: 0px 4px 8px 0px rgba(0, 0, 0, 0.32);
}
.screen-search-container .thumbnail-ui-box .button-row {
margin-right: 8px;
}
.screen-search-container .thumbnail-ui-box .avatar-container {
margin: auto;
position: relative;
}
.screen-search-container .thumbnail-ui-box .avatar-container > .avatar-widget {
float: right;
height: 64px;
}
.screen-search-container .thumbnail-ui-box .image-for-suggestions {
float: right;
margin-left: 25px;
}
.screen-search-container .thumbnail-ui-box .image-for-suggestions > img {
display: block;
height: 100px;
width: 100px;
object-fit: cover;
object-position: 50% 0;
border-radius: var(--border-radius-large);
/* matches the avatar display */
}
html[data-whats-new-updated] .screen-search-container .thumbnail-ui-box .settings-menu-box {
--button-color: #cc0;
}
.screen-search-container .following-tag {
text-decoration: none;
}
.search-results {
overflow-y: scroll;
overflow-x: clip;
scrollbar-gutter: stable both-edges;
}
html.has-overlay-scrollbars:not(.mobile) .search-view {
margin: 0 1em;
}
.search-view .flash a {
animation-name: flash-thumbnail;
animation-duration: 300ms;
animation-timing-function: ease-out;
animation-iteration-count: 1;
}
@keyframes flash-thumbnail {
0% {
filter: brightness(200%);
}
}
.search-view > .no-results {
display: flex;
justify-content: center;
align-items: center;
user-select: none;
-webkit-user-select: none;
margin: 0;
color: var(--frame-fg-color);
font-size: 1em;
pointer-events: none;
font-size: 1.5rem;
margin-top: 2em;
}
.search-view > .no-results > .message {
background-color: rgba(0, 0, 0, 0.5);
border-radius: var(--border-radius-large);
overflow: hidden;
text-align: center;
max-width: 40em;
max-height: 50vh;
padding: 0.25em 0.5em;
}
.search-view > .no-results > .message {
padding: 0.5em 1em;
}
.search-view .last-viewed-image-marker {
position: absolute;
left: 0;
top: 0;
pointer-events: none;
height: auto;
width: calc(var(--thumb-width) / 4);
}
.search-view .thumbnail-box:not(.flash) .last-viewed-image-marker {
display: none;
}
.search-view .thumbnail-box.expanded-thumb a.thumbnail-link {
border-radius: var(--border-radius-large);
}
.search-view .thumbnails {
user-select: none;
-webkit-user-select: none;
text-align: center;
row-gap: var(--thumb-padding);
width: var(--container-width);
display: flex;
flex-direction: column;
padding-bottom: var(--thumb-padding);
margin: 0 auto;
/* Add a stroke around the heart on thumbnails for visibility. Don't
* change the black lock. */
}
.search-view .thumbnails .row {
display: flex;
flex-direction: row;
column-gap: var(--thumb-padding);
align-items: flex-end;
justify-content: center;
}
.search-view .thumbnails .button-bookmark svg > .heart {
stroke: #000;
stroke-width: 0.5px;
}
html:not(.ios) .search-view .row {
content-visibility: auto;
contain-intrinsic-height: var(--row-height);
contain: strict;
}
.search-view .thumbnail-box {
position: relative;
width: var(--thumb-width);
height: var(--thumb-height);
flex-shrink: 0;
/* Hide pending images (they haven't been set up yet). */
}
.search-view .thumbnail-box .thumb {
object-fit: cover;
/* Show the top-center of the thumbnail. This generally makes more sense
* than cropping the center. */
object-position: 50% 0%;
width: 100%;
height: 100%;
}
.search-view .thumbnail-box[data-pending] {
visibility: hidden;
}
.search-view .thumbnail-box a.thumbnail-link {
display: block;
width: 100%;
height: 100%;
}
html:not(.mobile) .search-view .thumbnail-box a.thumbnail-link {
border-radius: var(--border-radius-large);
overflow: hidden;
}
@media (hover: hover) {
.search-view .thumbnail-box {
--zoom-thumb: 1;
--pan-thumb: paused;
}
body:not(.disable-thumbnail-zooming) .search-view .thumbnail-box:not(:hover) {
--zoom-thumb: 1.25;
}
.search-view .thumbnail-box .thumb {
transition: transform 0.5s;
transform: scale(var(--zoom-thumb));
}
body:not(.disable-thumbnail-panning) .search-view .thumbnail-box:hover {
--pan-thumb: running;
}
}
.search-view .thumbnail-box.muted {
overflow: hidden;
}
.search-view .thumbnail-box.muted .muted-text {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
pointer-events: none;
color: #fff;
}
.search-view .thumbnail-box.muted .muted-text .muted-icon {
opacity: 0.8;
display: block;
font-size: min(var(--thumb-height) * 0.25, 3rem);
}
.search-view .thumbnail-box.muted .muted-text .muted-label {
font-size: 1rem;
}
.search-view .thumbnail-box.muted .thumb {
filter: blur(5px) grayscale(0.2) brightness(0.3);
transform: scale(1.25, 1.25);
}
body:not(.disable-thumbnail-zooming) .search-view .thumbnail-box.muted .thumb:hover {
transform: scale(1, 1);
}
.search-view .thumbnail-box:not(.muted) .muted-text {
display: none;
}
@media (hover: hover) {
.search-view .thumbnail-box.expanded-manga-post:not(:hover):not(.first-manga-page) .manga-info-box {
display: none;
}
}
.search-view .thumbnail-box .bottom-row {
position: absolute;
display: flex;
align-items: end;
justify-content: center;
gap: 4px;
pointer-events: none;
width: 100%;
max-height: 75%;
bottom: 3px;
padding: 0 4px;
overflow: hidden;
}
html.mobile .search-view .thumbnail-box .bottom-row {
font-size: 1rem;
}
.search-view .thumbnail-box .bottom-row .bottom-left-icon,
.search-view .thumbnail-box .bottom-row .bottom-right-icon {
height: 32px;
width: 100px;
flex-shrink: 100000;
display: flex;
align-items: center;
gap: 0.25em;
justify-content: end;
align-self: end;
}
.search-view .thumbnail-box .bottom-row .bottom-right-icon {
margin-right: 8px;
}
.search-view .thumbnail-box .bottom-row .thumbnail-label {
display: flex;
align-items: center;
align-self: start;
gap: 0.5em;
flex-shrink: 1;
color: var(--frame-fg-color);
background-color: rgba(0, 0, 0, 0.6);
padding: 4px 8px;
overflow: hidden;
border-radius: 6px;
}
.search-view .thumbnail-box .bottom-row .thumbnail-label .thumbnail-ellipsis-box {
text-overflow: ellipsis;
overflow: hidden;
}
.search-view .thumbnail-box .bottom-row .thumbnail-label .thumbnail-ellipsis-box > .label {
/* Specify a line-height explicitly, so vertical centering is reasonably consistent for
* both EN and JP text. */
line-height: 19px;
}
.search-view .thumbnail-box .bottom-row .thumbnail-label .ugoira-icon {
color: #fff;
}
.search-view .thumbnail-box .bottom-row .heart {
width: 32px;
height: 32px;
}
.search-view .thumbnail-box .bottom-row .manga-info-box {
display: inline-flex;
align-items: center;
justify-content: center;
vertical-align: middle;
padding: 0.25em 0.5em;
background-color: rgba(0, 0, 0, 0.6);
border-radius: var(--border-radius-small) !important;
white-space: nowrap;
cursor: pointer;
overflow: hidden;
}
html:not(.mobile) .search-view .thumbnail-box .bottom-row .manga-info-box {
pointer-events: auto;
}
.search-view .thumbnail-box .bottom-row .manga-info-box:hover {
background-color: rgba(0, 20, 120, 0.8);
}
.search-view .thumbnail-box .bottom-row .manga-info-box:hover .regular {
display: none;
}
.search-view .thumbnail-box .bottom-row .manga-info-box:not(:hover) .hover {
display: none;
}
.search-view .thumbnail-box .bottom-row .manga-info-box .page-count {
vertical-align: middle;
padding-left: 0.15em;
}
html:not(.ios) .search-view .thumbnail-box .bottom-row .manga-info-box .page-count {
margin-bottom: -0.15em;
}
html.mobile .search-view .thumbnail-box .bottom-row .manga-info-box.show-expanded .page-icon {
display: none;
}
html.mobile .search-view .thumbnail-box .bottom-row .manga-info-box.show-expanded .page-count {
padding-left: 0;
margin-bottom: 0;
}
.search-view .thumbnail-box .bottom-row .ai-image {
height: 100%;
object-fit: contain;
padding: 4px;
image-rendering: pixelated;
}
@media (hover: hover) {
.search-view .thumbnail-box:hover .heart > svg,
.search-view .thumbnail-box:hover .ugoira-icon,
.search-view .thumbnail-box:hover .ai-image {
opacity: 0.5;
}
}
.search-view [data-type=order-shuffle] .icon {
font-size: 24px;
}
.search-view .artist-header {
display: flex;
flex-direction: column;
align-items: center;
width: var(--container-width);
padding-bottom: var(--thumb-padding);
margin: 0 auto;
user-select: none;
-webkit-user-select: none;
}
.search-view .artist-header .shape {
width: 100%;
height: min(20vh, var(--row-height));
overflow: hidden;
background-color: #333;
}
html:not(.mobile) .search-view .artist-header .shape {
border-radius: var(--border-radius-large);
}
.search-view .artist-header .shape img.bg {
width: 100%;
height: 100%;
object-fit: cover;
object-position: 50% 100%;
}
.search-view .artist-header .shape img.bg.loaded {
object-position: 50% 20%;
}
.search-view .artist-header .shape img.bg.loaded:not(.animated) {
object-position: 50% 40%;
}
.search-view .artist-header .shape img.bg.animated {
transition: object-position ease 5s;
}
.box-link-row {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5em;
}
.box-link-row > .box-link {
margin-right: 0.5em;
padding-inline: 0.5em;
}
.box-button-row {
display: flex;
flex-direction: row;
flex-wrap: unwrap;
overflow: auto;
align-items: center;
row-gap: 0.5em;
}
html.mobile .box-button-row {
justify-content: center;
}
.box-button-row.group {
column-gap: 1em;
}
.box-button-row > .box-link {
margin: 0 0.25em;
padding: 0 0.5em;
}
.box-button-row > .box-link > * {
padding: 0.25em 0;
}
.vertical-list {
--box-link-bg-color: rgba(0, 0, 0, 0);
user-select: none;
-webkit-user-select: none;
}
.vertical-list > .box-link {
padding-left: 0.5em;
padding-right: 0.5em;
}
.vertical-list > .box-link {
display: flex;
flex-direction: row;
align-items: center;
margin-top: 0;
margin-bottom: 0;
}
.box-link:hover {
background-color: var(--box-link-hover-color);
}
.box-link {
border-radius: var(--border-radius-large);
display: inline-flex;
cursor: pointer;
text-decoration: none;
margin: 0;
padding: 0 0.75em;
align-content: center;
align-items: center;
height: 2em;
line-height: 1.5em;
color: var(--box-link-fg-color);
user-select: none;
-webkit-user-select: none;
background-color: var(--box-link-bg-color);
}
.box-link .label-box {
flex: 1;
display: flex;
align-items: center;
}
.box-link .label {
vertical-align: middle;
flex: 1;
}
.box-link:not(.allow-wrap) {
white-space: nowrap;
}
.box-link.selected {
background-color: var(--box-link-selected-color);
}
@media (hover: hover) {
.box-link:not(.disabled):hover {
background-color: var(--box-link-hover-color);
}
.box-link:not(.disabled):hover.selected {
background-color: var(--box-link-selected-hover-color);
}
}
.box-link:not(.disabled):active {
background-color: var(--box-link-hover-color);
}
.box-link:not(.disabled):active.selected {
background-color: var(--box-link-selected-hover-color);
}
.box-link.disabled {
color: var(--box-link-disabled-color);
cursor: auto;
pointer-events: none;
}
.box-link.tag {
/* Some tags are way too long, since translations don't put any sanity limit on length.
* Cut these off so they don't break the layout. */
max-width: 100%;
text-overflow: ellipsis;
overflow: hidden;
}
.box-link .icon {
display: inline-block;
font-size: inherit;
vertical-align: middle;
}
.box-link .icon.with-text {
margin-right: 0.25em;
width: 1ch;
}
.box-link .icon:not(.with-text) {
font-size: 150%;
}
.search-box {
display: flex;
}
.tag-search-box {
display: flex;
flex-wrap: wrap;
align-items: center;
position: relative;
gap: 0.5em;
}
.tag-search-box > .search-history {
position: absolute;
left: 0;
bottom: 0;
}
.input-field-container {
flex: 1;
padding: 6px 10px;
outline: 1px solid var(--input-outline);
display: inline-flex;
gap: 0.5em;
align-items: center;
}
.input-field-container:focus-within {
outline: 1px solid var(--input-outline-focused);
}
.input-field-container > input {
outline: none;
font-size: 1.2em;
vertical-align: middle;
flex: 1;
min-width: 0;
}
.input-field-container > .right-side-button {
display: flex;
font-size: 150%;
vertical-align: middle;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
.input-field-container > .right-side-button.disabled {
color: #888;
}
/* Search box in the menu: */
.navigation-search-box .search-submit-button {
vertical-align: middle;
margin-left: -30px;
/* overlap the search box */
}
.navigation-search-box input.search-tags {
width: 100%;
padding-right: 30px;
/* extra space for the submit button */
}
.viewer-error .muted-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
filter: blur(20px);
opacity: 0.75;
}
.viewer-error .error-text-container {
position: absolute;
width: 100%;
top: 50%;
left: 0;
text-align: center;
font-size: 30px;
color: #000;
text-shadow: 0px 1px 1px #fff, 0px -1px 1px #fff, 1px 0px 1px #fff, -1px 0px 1px #fff;
}
.tag-list.box-button-row.widget {
padding-block-end: 12px;
margin: 0 8px 0 4px;
}
.data-source-tag-list {
max-height: 50vh;
min-width: min(20em, 100vw);
max-width: 100vw;
overflow-x: hidden;
overflow-y: auto;
white-space: nowrap;
}
.data-source-tag-list .recent {
--box-link-bg-color: #550;
--box-link-hover-color: #660;
}
.data-source-tag-list .tag-entry:hover:after {
left: auto;
right: 0px;
}
.input-dropdown {
user-select: none;
-webkit-user-select: none;
resize: horizontal;
width: var(--width);
max-width: min(800px, 100vw);
}
.input-dropdown:not(.editing) .edit-button,
.input-dropdown:not(.editing) .editing-only {
display: none !important;
}
.input-dropdown .edit-button,
.input-dropdown .group-suggestions-button {
align-items: center;
justify-content: center;
border: 1px solid #bbb;
height: 1.5em;
width: 1.5em;
cursor: pointer;
display: flex;
}
.input-dropdown .edit-button.selected,
.input-dropdown .group-suggestions-button.selected {
color: #000;
background-color: #ccc;
}
@media (hover: hover) {
.input-dropdown .edit-button:hover,
.input-dropdown .group-suggestions-button:hover {
border-color: #fff;
}
}
.input-dropdown.loading {
pointer-events: none;
opacity: 0;
}
.input-dropdown .input-dropdown-list {
margin: 1px;
display: flex;
flex-direction: column;
white-space: normal;
margin-right: 3px;
}
.input-dropdown .input-dropdown-list .tag-section:not(.user-section) .user-section-edit-button {
display: none !important;
}
.input-dropdown .input-dropdown-list .tag-section.autocomplete .delete-entry {
display: none !important;
}
.input-dropdown .input-dropdown-list .tag-section {
position: sticky;
top: 0;
width: 100%;
display: flex;
gap: 0.5em;
align-items: center;
background-color: #001a44;
padding: 0.5em 1em;
margin-bottom: 5px;
cursor: pointer;
line-height: 1.5em;
}
.input-dropdown .input-dropdown-list .tag-section .label,
.input-dropdown .input-dropdown-list .tag-section .label-edit {
flex: 1;
}
.input-dropdown .input-dropdown-list.editing .tag-section {
padding: 0.5em;
}
.input-dropdown .input-dropdown-list .entry {
display: flex;
flex-direction: row;
color: var(--box-link-fg-color);
align-items: center;
gap: 0.5em;
padding: 0.5em;
border-radius: var(--border-radius-large);
}
.input-dropdown .input-dropdown-list .entry.recent .edit-button[data-shown-in~=recent] {
display: flex;
}
.input-dropdown .input-dropdown-list .entry.saved .edit-button[data-shown-in~=saved] {
display: flex;
}
.input-dropdown .input-dropdown-list .entry.autocomplete .edit-button[data-shown-in~=autocomplete] {
display: flex;
}
.input-dropdown .input-dropdown-list .entry .edit-button {
display: none;
}
.input-dropdown .input-dropdown-list .entry .search {
flex: 1;
height: 100%;
display: inline-flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 0.5em;
}
.input-dropdown .input-dropdown-list .entry .search .word {
display: inline-flex;
align-items: center;
height: 100%;
line-height: 1em;
}
.input-dropdown .input-dropdown-list .entry .search .word.or {
font-size: 12px;
color: #aaa;
}
.input-dropdown .input-dropdown-list .entry.selected {
background-color: var(--box-link-selected-color);
}
@media (hover: hover) {
.input-dropdown .input-dropdown-list:not(.editing) .entry:hover {
background-color: var(--box-link-hover-color);
}
.input-dropdown .input-dropdown-list:not(.editing) .entry:hover.selected {
background-color: var(--box-link-selected-hover-color);
}
}
.input-dropdown .input-dropdown-list.editing .entry {
cursor: default;
}
.widget.hidden-widget {
display: none;
}
/* The right click context menu for the image view: */
#popup-context-menu {
color: #fff;
position: fixed;
text-align: left;
padding: 4px;
border: 1px solid var(--ui-border-color);
border-radius: var(--border-radius-large);
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
z-index: 10;
user-select: none;
-webkit-user-select: none;
will-change: opacity, transform;
transition: opacity ease 0.15s, transform ease 0.15s;
--button-size: 32px;
--button-size-small: 22px;
/* Hide the normal tooltips. The context menu shows them differently. */
}
#popup-context-menu.hidden-widget {
display: inherit;
opacity: 0;
pointer-events: none;
transform: scale(0.85);
}
#popup-context-menu:not(.hidden-widget) {
opacity: 1;
}
#popup-context-menu .popup:hover:after {
display: none;
}
#popup-context-menu .avatar-widget {
transition: filter 0.25s;
display: block;
position: relative;
height: inherit;
filter: contrast(1);
}
#popup-context-menu .inline-tooltip {
margin-top: 4px;
font-size: 11px;
color: #ccc;
text-align: center;
white-space: nowrap;
pointer-events: none;
user-select: none;
}
#popup-context-menu .context-menu-item {
display: flex;
border-radius: var(--border-radius-small);
height: var(--button-size);
padding-inline: 8px;
align-items: center;
gap: 4px;
}
#popup-context-menu .context-menu-item:hover {
background-color: var(--button-hover-bgcolor);
}
#popup-context-menu .tooltip-display {
position: fixed;
pointer-events: none;
background: rgba(0, 0, 0, 0.85);
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
z-index: 9999;
}
#popup-context-menu .tooltip-display .tooltip-display-text {
display: inline;
background-color: var(--frame-bg-color);
color: var(--frame-fg-color);
padding: 2px 8px;
border-radius: var(--border-radius-large);
white-space: nowrap;
}
#popup-context-menu .tooltip-display .tooltip-display-text:after {
content: attr(data-popup);
}
#popup-context-menu #context-menu-buttons-group {
display: grid;
max-height: 500px;
gap: 2px;
/* Remove the double horizontal padding: */
/* Remove the double vertical padding. Do this with a negative margin instead of zeroing
* the padding, so the rounded black background stays the same size. */
}
#popup-context-menu #context-menu-buttons-group > .button-block {
display: inline-flex;
flex-direction: column;
background-color: var(--context-menu-bg-color);
padding: 12px;
box-sizing: content-box;
justify-content: stretch;
align-items: center;
}
#popup-context-menu #context-menu-buttons-group > .button-block:not(:first-child) {
padding-left: 0px;
}
#popup-context-menu #context-menu-buttons-group:not(:last-child) > .button-block {
margin-bottom: -12px;
}
#popup-context-menu #context-menu-buttons-group .context-menu-item-description {
display: block;
font-size: 0.8em;
width: inherit;
}
#popup-context-menu #context-menu-buttons-group .count {
color: white;
text-shadow: 0px 1px 1px var(--minor-text-shadow-color), 0px -1px 1px var(--minor-text-shadow-color), 1px 0px 1px var(--minor-text-shadow-color), -1px 0px 1px var(--minor-text-shadow-color);
font-size: 0.7em;
font-weight: bold;
width: 100%;
position: absolute;
}
#popup-context-menu #context-menu-buttons-group svg {
height: var(--button-size-small);
width: var(--button-size-small);
margin-right: 4px;
}
#popup-context-menu #context-menu-buttons-group .button {
height: var(--button-size);
cursor: pointer;
display: flex;
/* Grey out the buttons if this strip isn't enabled. */
/* We don't have a way to add classes to inlined SVGs yet, so for now just use nth-child.
The first child is the + icon and the second child is -. */
/* Popup menu bookmarking */
}
#popup-context-menu #context-menu-buttons-group .button .font-icon {
font-size: var(--button-size-small);
margin-right: 4px;
}
#popup-context-menu #context-menu-buttons-group .button:not(.enabled) {
cursor: inherit;
color: #666;
}
@media (hover: hover) {
#popup-context-menu #context-menu-buttons-group .button.enabled:hover {
color: #fff;
background-color: var(--button-hover-bgcolor);
}
}
#popup-context-menu #context-menu-buttons-group .button.enabled.selected {
background-color: var(--button-hover-bgcolor);
color: #fff;
}
#popup-context-menu #context-menu-buttons-group .button.button-zoom:not(.selected) > :nth-child(1) {
display: none;
}
#popup-context-menu #context-menu-buttons-group .button.button-zoom.selected > :nth-child(2) {
display: none;
}
#popup-context-menu #context-menu-buttons-group .button .tag-dropdown-arrow {
width: 0;
height: 0;
border-top: 10px solid #222;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
}
#popup-context-menu #context-menu-buttons-group > .button-block.shift-right {
margin-left: calc(var(--button-size) + 12px + 12px);
}
#popup-context-menu #context-menu-buttons-group > .button-block.shift-left {
margin-left: calc(-1 * (var(--button-size) + 12px + 12px));
}
#popup-context-menu #context-menu-image-info-container {
align-self: flex-end;
background-color: var(--context-menu-bg-color);
padding-right: 8px;
user-select: none;
-webkit-user-select: none;
}
#popup-context-menu #context-menu-image-info {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
#popup-context-menu .popup-bookmark-tag-dropdown {
right: -100%;
}
.popup-more-options-container .button-send-image svg .arrow {
transition: transform ease-in-out 0.15s;
}
.popup-more-options-container .button-send-image:not(.disabled):hover svg .arrow {
transform: translate(2px, -2px);
}
.bookmark-tag-list,
.more-options-dropdown {
/* In the context menu version, nudge the tag dropdown up slightly to cover
* the rounded corners. */
/* Recent bookmark tags in the popup menu: */
}
#popup-context-menu .bookmark-tag-list,
#popup-context-menu .more-options-dropdown {
top: calc(100% - 4px);
}
.bookmark-tag-list > .tag-list,
.more-options-dropdown > .tag-list {
min-width: 200px;
}
.bookmark-tag-list .popup-bookmark-tag-entry,
.more-options-dropdown .popup-bookmark-tag-entry {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.25em 0.5em;
cursor: pointer;
background-color: var(--box-link-bg-color);
}
.bookmark-tag-list .popup-bookmark-tag-entry > .tag-name,
.more-options-dropdown .popup-bookmark-tag-entry > .tag-name {
flex: 1;
display: flex;
gap: 0.5em;
}
.bookmark-tag-list .popup-bookmark-tag-entry.selected,
.more-options-dropdown .popup-bookmark-tag-entry.selected {
background-color: var(--box-link-selected-color);
}
@media (hover: hover) {
.bookmark-tag-list .popup-bookmark-tag-entry:hover,
.more-options-dropdown .popup-bookmark-tag-entry:hover {
background-color: var(--box-link-hover-color);
}
.bookmark-tag-list .popup-bookmark-tag-entry:hover.selected,
.more-options-dropdown .popup-bookmark-tag-entry:hover.selected {
background-color: var(--box-link-selected-hover-color);
}
}
.mobile-illust-ui-dialog {
--box-link-bg-color: none;
}
.mobile-illust-ui-container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
pointer-events: none;
--pointer-events: none;
--frame-bg-color: rgba(0, 0, 0, 0.65);
--box-link-bg-color: none;
}
.mobile-illust-ui-container.fully-visible {
--pointer-events: auto;
}
.mobile-illust-ui-container .mobile-illust-ui-page {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
user-select: none;
-webkit-user-select: none;
translate: 0 calc((var(--menu-bar-pos) - 0) * (0px - var(--menu-bar-height)) / 1 + var(--menu-bar-height));
}
.mobile-illust-ui-container .mobile-illust-ui-page .avatar {
align-self: center;
}
.mobile-illust-ui-container .mobile-illust-ui-page .avatar-widget {
opacity: var(--menu-bar-pos);
pointer-events: var(--pointer-events);
margin-bottom: 1em;
width: min(170px, 50vmin);
}
.mobile-illust-ui-container .mobile-illust-ui-page .avatar-widget > .avatar {
border: 1px solid #000;
}
.mobile-illust-ui-container .mobile-illust-ui-page .avatar-widget .follow-icon {
bottom: 0;
right: 0;
}
.mobile-illust-ui-container .mobile-illust-ui-page .menu-bar {
opacity: var(--menu-bar-pos);
pointer-events: var(--pointer-events);
padding-top: 0.5em;
padding-bottom: max(env(safe-area-inset-bottom), 0.5em);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
font-size: 65%;
display: flex;
flex-direction: row;
justify-content: space-evenly;
}
.mobile-illust-ui-container .item {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25em;
padding: 0 0.25em;
justify-content: center;
--foreground-color: #aaa;
color: var(--foreground-color);
/* Popup menu bookmarking */
}
.mobile-illust-ui-container .item.enabled {
--foreground-color: #fff;
}
.mobile-illust-ui-container .item.selected {
--foreground-color: #ff8;
}
.mobile-illust-ui-container .item > svg {
width: 2em;
height: 2em;
}
.mobile-illust-ui-container .item .label {
text-align: center;
font-size: 0.7rem;
}
.mobile-illust-ui-container .item .tag-dropdown-arrow {
width: 0;
height: 0;
border-top: 10px solid #222;
border-left: 10px solid transparent;
border-right: 10px solid transparent;
}
.mobile-illust-ui-container .item .font-icon {
font-size: 200%;
}
.mobile-image-info {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.mobile-image-info .author-block {
display: inline-flex;
align-items: center;
}
.mobile-image-info .author-block .avatar-widget {
height: inherit;
margin-right: 0.5em;
}
.mobile-image-info .description {
display: inline-block;
width: 100%;
background-color: #000;
padding: 8px;
border: 1px solid #aaa;
overflow-wrap: break-word;
}
.mobile-image-info .bookmark-tags {
margin-top: 0.25em;
display: flex;
flex-wrap: wrap;
flex-direction: row;
gap: 0.25em 0.5em;
justify-content: center;
}
.mobile-image-info .bookmark-tags .mobile-ui-tag-entry {
white-space: nowrap;
background-color: var(--frame-bg-color);
padding: 0.5em 0.75em;
border-radius: var(--border-radius-large);
line-height: 1.5em;
flex: 1;
}
.mobile-image-info .bookmark-tags .mobile-ui-tag-entry .bookmark-tag-icon {
vertical-align: middle;
}
.button-bookmark,
.button-like {
position: relative;
}
/* Hide the "delete" stroke over the heart icon unless clicking the button will
* remove the bookmark. */
svg.heart-image .delete {
display: none;
}
@media (hover: hover) {
.button-bookmark.button.will-delete.enabled:hover svg.heart-image .delete {
display: inline;
}
}
@media (hover: hover) {
.button-bookmark.enabled:hover svg {
color: #f00 !important;
stroke: none;
}
}
.button.button-like {
/* This is a pain due to transition bugs in Firefox. It doesn't like having
* transition: transform on both an SVG and on individual paths inside the
* SVG and clips the image incorrectly during the animation. Work around this
* by only placing transitions on the paths. */
}
.button.button-like > svg {
color: var(--like-button-color);
}
.button.button-like.liked > svg {
color: var(--like-button-liked-color);
}
@media (hover: hover) {
.button.button-like.enabled:hover > svg {
color: var(--like-button-hover-color);
}
}
.button.button-browser-back .arrow {
transition: transform ease-in-out 0.15s;
transform: translate(-2px, 0px);
}
@media (hover: hover) {
.button.button-browser-back:hover .arrow {
transform: translate(1px, 0px);
}
}
.button.button-like {
--overall-translate-x: 0;
--overall-translate-y: 0;
--mouth-scale-x: 1;
--mouth-scale-y: 0.75;
--mouth-translate-x: 0;
--mouth-translate-y: 0;
}
.button.button-like.liked {
--mouth-scale-x: 1;
--mouth-scale-y: 1.1;
--mouth-translate-x: 0;
--mouth-translate-y: -3px;
--overall-translate-x: 0;
--overall-translate-y: -3px;
}
@media (hover: hover) {
.button.button-like.enabled:hover {
--overall-translate-x: 0;
--overall-translate-y: -2px;
--mouth-scale-x: 1;
--mouth-scale-y: 0.9;
--mouth-translate-x: 0;
--mouth-translate-y: -3px;
}
}
.button.button-like > svg > * {
transition: transform ease-in-out 0.15s;
transform: translate(var(--overall-translate-x), var(--overall-translate-y));
}
.button.button-like > svg > .mouth {
transform: scale(var(--mouth-scale-x), var(--mouth-scale-y)) translate(var(--mouth-translate-x), var(--mouth-translate-y));
}
.button-bookmark.public svg.heart-image .lock {
display: none;
}
.button-bookmark svg.heart-image .lock {
stroke: #888;
}
.dialog-normal {
position: fixed;
z-index: 1000;
inset: 0;
overscroll-behavior: contain;
--dialog-visible: 1;
display: flex;
align-items: flex-end;
justify-content: center;
--dialog-bg-color: rgba(0, 0, 0, var(--dialog-bg-alpha));
background-color: rgba(0, 0, 0, min(var(--dialog-backdrop-alpha), max(0, (var(--dialog-visible) - 0) * (var(--dialog-backdrop-alpha) - 0) / 1 + 0)));
--edge-padding-inner-horiz: 20px;
--edge-padding-inner-vert: 20px;
--edge-padding-outer-horiz: 40px;
--edge-padding-outer-vert: 20px;
padding: var(--dialog-outer-padding-top) var(--dialog-outer-padding-right) var(--dialog-outer-padding-bottom) var(--dialog-outer-padding-left);
}
html.ios .dialog-normal {
inset: -1px 0;
}
.dialog-normal.floating {
align-items: center;
}
.dialog-normal:not(.floating) {
--dialog-bg-alpha: 0.5;
--dialog-backdrop-alpha: 0.5;
}
.dialog-normal.floating {
--dialog-bg-alpha: 0.75;
--dialog-backdrop-alpha: 0.5;
}
html.mobile .dialog-normal {
font-size: 1.2rem;
}
html.mobile .dialog-normal {
--edge-padding-inner-horiz: 10px;
--edge-padding-inner-vert: 10px;
}
.dialog-normal.floating {
--dialog-outer-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-top: calc(max(env(safe-area-inset-top), var(--edge-padding-outer-vert)));
--dialog-outer-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-outer-vert)));
--dialog-inner-padding-left: var(--edge-padding-inner-horiz);
--dialog-inner-padding-right: var(--edge-padding-inner-horiz);
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: var(--edge-padding-inner-vert);
}
.dialog-normal.floating .dialog {
opacity: min(1, max(0, (var(--dialog-visible) - 0) * 1 / 1 + 0));
}
.dialog-normal:not(.floating) {
--edge-padding-horiz: 20px;
--edge-padding-vert: 20px;
--dialog-extra-top-padding: 2vh;
--dialog-outer-padding-left: 0px;
--dialog-outer-padding-right: 0px;
--dialog-outer-padding-top: calc(env(safe-area-inset-top) + var(--dialog-extra-top-padding));
--dialog-outer-padding-bottom: 0px;
--dialog-inner-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-inner-vert)));
}
.dialog-normal:not(.floating) .dialog {
border-radius: var(--border-radius-large);
overflow: hidden;
}
.dialog-normal > .dialog {
padding: var(--dialog-inner-padding-top) var(--dialog-inner-padding-right) var(--dialog-inner-padding-bottom) var(--dialog-inner-padding-left);
transform: translateY(calc((var(--dialog-visible) - 0) * -100% / 1 + 100%));
}
.dialog-normal > .dialog .scroll {
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
margin-right: calc(var(--dialog-inner-padding-right) * -1);
padding-right: calc(var(--dialog-inner-padding-right));
margin-left: calc(var(--dialog-inner-padding-left) * -1);
padding-left: calc(var(--dialog-inner-padding-left));
margin-bottom: calc(var(--dialog-inner-padding-bottom) * -1);
padding-bottom: calc(var(--dialog-inner-padding-bottom));
}
.dialog-normal.floating .dialog {
min-width: 20em;
max-width: min(45em, 90vw);
max-height: 90vh;
border-radius: var(--border-radius-large);
}
.dialog-normal.small .dialog {
min-width: 10em;
max-width: 45em;
}
.dialog-normal:not(.floating) .dialog {
width: 100%;
}
.dialog-normal .dialog {
max-height: 100%;
border: solid 1px var(--ui-border-color);
border-radius: var(--border-radius-large);
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--ui-fg-color);
position: relative;
display: flex;
flex-direction: column;
}
.dialog-normal.dragging-dialog > .dialog {
pointer-events: none;
}
.dialog-normal.dragging-dialog .vertical-scroller {
overflow: hidden !important;
}
.dialog-normal .header {
--icon-size: 2;
line-height: 2em;
display: flex;
align-items: center;
}
.dialog-normal .header .close-button-container {
flex: 1;
}
.dialog-normal .header .close-button-container .close-button {
--icon-size: 1.5;
color: var(--button-color);
cursor: pointer;
justify-self: center;
}
@media (hover: hover) {
.dialog-normal .header .close-button-container .close-button:hover {
color: var(--button-highlight-color);
}
}
.dialog-normal .header .close-button-container .close-button > svg {
display: block;
}
.dialog-normal .header .header-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.dialog-normal .header .center-header-helper {
width: calc(1em * var(--icon-size) + 1em);
flex: 1;
flex-shrink: 100000;
}
.dialog-small {
position: fixed;
z-index: 1000;
inset: 0;
overscroll-behavior: contain;
--dialog-visible: 1;
display: flex;
align-items: flex-end;
justify-content: center;
--dialog-bg-color: rgba(0, 0, 0, var(--dialog-bg-alpha));
background-color: rgba(0, 0, 0, min(var(--dialog-backdrop-alpha), max(0, (var(--dialog-visible) - 0) * (var(--dialog-backdrop-alpha) - 0) / 1 + 0)));
--edge-padding-inner-horiz: 20px;
--edge-padding-inner-vert: 20px;
--edge-padding-outer-horiz: 40px;
--edge-padding-outer-vert: 20px;
padding: var(--dialog-outer-padding-top) var(--dialog-outer-padding-right) var(--dialog-outer-padding-bottom) var(--dialog-outer-padding-left);
}
html.ios .dialog-small {
inset: -1px 0;
}
.dialog-small.floating {
align-items: center;
}
.dialog-small:not(.floating) {
--dialog-bg-alpha: 0.5;
--dialog-backdrop-alpha: 0.5;
}
.dialog-small.floating {
--dialog-bg-alpha: 0.75;
--dialog-backdrop-alpha: 0.5;
}
html.mobile .dialog-small {
font-size: 1.2rem;
}
html.mobile .dialog-small {
--edge-padding-inner-horiz: 10px;
--edge-padding-inner-vert: 10px;
}
.dialog-small.floating {
--dialog-outer-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-top: calc(max(env(safe-area-inset-top), var(--edge-padding-outer-vert)));
--dialog-outer-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-outer-vert)));
--dialog-inner-padding-left: var(--edge-padding-inner-horiz);
--dialog-inner-padding-right: var(--edge-padding-inner-horiz);
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: var(--edge-padding-inner-vert);
}
.dialog-small.floating .dialog {
opacity: min(1, max(0, (var(--dialog-visible) - 0) * 1 / 1 + 0));
}
.dialog-small:not(.floating) {
--edge-padding-horiz: 20px;
--edge-padding-vert: 20px;
--dialog-extra-top-padding: 2vh;
--dialog-outer-padding-left: 0px;
--dialog-outer-padding-right: 0px;
--dialog-outer-padding-top: calc(env(safe-area-inset-top) + var(--dialog-extra-top-padding));
--dialog-outer-padding-bottom: 0px;
--dialog-inner-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-inner-vert)));
}
.dialog-small:not(.floating) .dialog {
border-radius: var(--border-radius-large);
overflow: hidden;
}
.dialog-small > .dialog {
padding: var(--dialog-inner-padding-top) var(--dialog-inner-padding-right) var(--dialog-inner-padding-bottom) var(--dialog-inner-padding-left);
transform: translateY(calc((var(--dialog-visible) - 0) * -100% / 1 + 100%));
}
.dialog-small > .dialog .scroll {
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
margin-right: calc(var(--dialog-inner-padding-right) * -1);
padding-right: calc(var(--dialog-inner-padding-right));
margin-left: calc(var(--dialog-inner-padding-left) * -1);
padding-left: calc(var(--dialog-inner-padding-left));
margin-bottom: calc(var(--dialog-inner-padding-bottom) * -1);
padding-bottom: calc(var(--dialog-inner-padding-bottom));
}
.dialog-small.floating .dialog {
min-width: 20em;
max-width: min(45em, 90vw);
max-height: 90vh;
border-radius: var(--border-radius-large);
}
.dialog-small.small .dialog {
min-width: 10em;
max-width: 45em;
}
.dialog-small:not(.floating) .dialog {
width: 100%;
}
.dialog-small .dialog {
max-height: 100%;
border: solid 1px var(--ui-border-color);
border-radius: var(--border-radius-large);
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--ui-fg-color);
position: relative;
display: flex;
flex-direction: column;
}
.dialog-small.dragging-dialog > .dialog {
pointer-events: none;
}
.dialog-small.dragging-dialog .vertical-scroller {
overflow: hidden !important;
}
.dialog-small .header {
--icon-size: 2;
line-height: 2em;
display: flex;
align-items: center;
}
.dialog-small .header .close-button-container {
flex: 1;
}
.dialog-small .header .close-button-container .close-button {
--icon-size: 1.5;
color: var(--button-color);
cursor: pointer;
justify-self: center;
}
@media (hover: hover) {
.dialog-small .header .close-button-container .close-button:hover {
color: var(--button-highlight-color);
}
}
.dialog-small .header .close-button-container .close-button > svg {
display: block;
}
.dialog-small .header .header-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.dialog-small .header .center-header-helper {
width: calc(1em * var(--icon-size) + 1em);
flex: 1;
flex-shrink: 100000;
}
.simple-button-dialog {
position: fixed;
z-index: 1000;
inset: 0;
overscroll-behavior: contain;
--dialog-visible: 1;
display: flex;
align-items: flex-end;
justify-content: center;
--dialog-bg-color: rgba(0, 0, 0, var(--dialog-bg-alpha));
background-color: rgba(0, 0, 0, min(var(--dialog-backdrop-alpha), max(0, (var(--dialog-visible) - 0) * (var(--dialog-backdrop-alpha) - 0) / 1 + 0)));
--edge-padding-inner-horiz: 20px;
--edge-padding-inner-vert: 20px;
--edge-padding-outer-horiz: 40px;
--edge-padding-outer-vert: 20px;
padding: var(--dialog-outer-padding-top) var(--dialog-outer-padding-right) var(--dialog-outer-padding-bottom) var(--dialog-outer-padding-left);
background: rgba(0, 0, 0, 0.2666666667);
}
html.ios .simple-button-dialog {
inset: -1px 0;
}
.simple-button-dialog.floating {
align-items: center;
}
.simple-button-dialog:not(.floating) {
--dialog-bg-alpha: 0.5;
--dialog-backdrop-alpha: 0.5;
}
.simple-button-dialog.floating {
--dialog-bg-alpha: 0.75;
--dialog-backdrop-alpha: 0.5;
}
html.mobile .simple-button-dialog {
font-size: 1.2rem;
}
html.mobile .simple-button-dialog {
--edge-padding-inner-horiz: 10px;
--edge-padding-inner-vert: 10px;
}
.simple-button-dialog.floating {
--dialog-outer-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-top: calc(max(env(safe-area-inset-top), var(--edge-padding-outer-vert)));
--dialog-outer-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-outer-vert)));
--dialog-inner-padding-left: var(--edge-padding-inner-horiz);
--dialog-inner-padding-right: var(--edge-padding-inner-horiz);
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: var(--edge-padding-inner-vert);
}
.simple-button-dialog.floating .dialog {
opacity: min(1, max(0, (var(--dialog-visible) - 0) * 1 / 1 + 0));
}
.simple-button-dialog:not(.floating) {
--edge-padding-horiz: 20px;
--edge-padding-vert: 20px;
--dialog-extra-top-padding: 2vh;
--dialog-outer-padding-left: 0px;
--dialog-outer-padding-right: 0px;
--dialog-outer-padding-top: calc(env(safe-area-inset-top) + var(--dialog-extra-top-padding));
--dialog-outer-padding-bottom: 0px;
--dialog-inner-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-inner-vert)));
}
.simple-button-dialog:not(.floating) .dialog {
border-radius: var(--border-radius-large);
overflow: hidden;
}
.simple-button-dialog > .dialog {
padding: var(--dialog-inner-padding-top) var(--dialog-inner-padding-right) var(--dialog-inner-padding-bottom) var(--dialog-inner-padding-left);
transform: translateY(calc((var(--dialog-visible) - 0) * -100% / 1 + 100%));
}
.simple-button-dialog > .dialog .scroll {
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
margin-right: calc(var(--dialog-inner-padding-right) * -1);
padding-right: calc(var(--dialog-inner-padding-right));
margin-left: calc(var(--dialog-inner-padding-left) * -1);
padding-left: calc(var(--dialog-inner-padding-left));
margin-bottom: calc(var(--dialog-inner-padding-bottom) * -1);
padding-bottom: calc(var(--dialog-inner-padding-bottom));
}
.simple-button-dialog.floating .dialog {
min-width: 20em;
max-width: min(45em, 90vw);
max-height: 90vh;
border-radius: var(--border-radius-large);
}
.simple-button-dialog.small .dialog {
min-width: 10em;
max-width: 45em;
}
.simple-button-dialog:not(.floating) .dialog {
width: 100%;
}
.simple-button-dialog .dialog {
max-height: 100%;
border: solid 1px var(--ui-border-color);
border-radius: var(--border-radius-large);
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--ui-fg-color);
position: relative;
display: flex;
flex-direction: column;
}
.simple-button-dialog.dragging-dialog > .dialog {
pointer-events: none;
}
.simple-button-dialog.dragging-dialog .vertical-scroller {
overflow: hidden !important;
}
.simple-button-dialog .header {
--icon-size: 2;
line-height: 2em;
display: flex;
align-items: center;
}
.simple-button-dialog .header .close-button-container {
flex: 1;
}
.simple-button-dialog .header .close-button-container .close-button {
--icon-size: 1.5;
color: var(--button-color);
cursor: pointer;
justify-self: center;
}
@media (hover: hover) {
.simple-button-dialog .header .close-button-container .close-button:hover {
color: var(--button-highlight-color);
}
}
.simple-button-dialog .header .close-button-container .close-button > svg {
display: block;
}
.simple-button-dialog .header .header-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.simple-button-dialog .header .center-header-helper {
width: calc(1em * var(--icon-size) + 1em);
flex: 1;
flex-shrink: 100000;
}
.simple-button-dialog .dialog {
padding: 0;
background: none;
--box-link-bg-color: #0004;
--box-link-hover-color: #0008;
}
.simple-button-dialog .dialog .box-link {
padding: 1.5em 2em;
}
.whats-new-dialog {
display: flex;
flex-direction: column;
}
.whats-new-dialog .font-icon {
vertical-align: bottom;
}
.whats-new-dialog .rev {
display: inline-block;
color: var(--box-link-fg-color);
background-color: var(--box-link-bg-color);
padding: 5px 10px;
}
.whats-new-dialog .text {
margin: 1em 0;
padding: 0 20px;
/* inset horizontally a bit */
}
.whats-new-dialog .explanation-button {
cursor: pointer;
text-decoration: underline;
}
.whats-new-dialog .explanation-target {
margin: 0 1em;
}
.mobile-tag-list {
position: fixed;
z-index: 1000;
inset: 0;
overscroll-behavior: contain;
--dialog-visible: 1;
display: flex;
align-items: flex-end;
justify-content: center;
--dialog-bg-color: rgba(0, 0, 0, var(--dialog-bg-alpha));
background-color: rgba(0, 0, 0, min(var(--dialog-backdrop-alpha), max(0, (var(--dialog-visible) - 0) * (var(--dialog-backdrop-alpha) - 0) / 1 + 0)));
--edge-padding-inner-horiz: 20px;
--edge-padding-inner-vert: 20px;
--edge-padding-outer-horiz: 40px;
--edge-padding-outer-vert: 20px;
padding: var(--dialog-outer-padding-top) var(--dialog-outer-padding-right) var(--dialog-outer-padding-bottom) var(--dialog-outer-padding-left);
--box-link-bg-color: none;
}
html.ios .mobile-tag-list {
inset: -1px 0;
}
.mobile-tag-list.floating {
align-items: center;
}
.mobile-tag-list:not(.floating) {
--dialog-bg-alpha: 0.5;
--dialog-backdrop-alpha: 0.5;
}
.mobile-tag-list.floating {
--dialog-bg-alpha: 0.75;
--dialog-backdrop-alpha: 0.5;
}
html.mobile .mobile-tag-list {
font-size: 1.2rem;
}
html.mobile .mobile-tag-list {
--edge-padding-inner-horiz: 10px;
--edge-padding-inner-vert: 10px;
}
.mobile-tag-list.floating {
--dialog-outer-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-top: calc(max(env(safe-area-inset-top), var(--edge-padding-outer-vert)));
--dialog-outer-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-outer-vert)));
--dialog-inner-padding-left: var(--edge-padding-inner-horiz);
--dialog-inner-padding-right: var(--edge-padding-inner-horiz);
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: var(--edge-padding-inner-vert);
}
.mobile-tag-list.floating .dialog {
opacity: min(1, max(0, (var(--dialog-visible) - 0) * 1 / 1 + 0));
}
.mobile-tag-list:not(.floating) {
--edge-padding-horiz: 20px;
--edge-padding-vert: 20px;
--dialog-extra-top-padding: 2vh;
--dialog-outer-padding-left: 0px;
--dialog-outer-padding-right: 0px;
--dialog-outer-padding-top: calc(env(safe-area-inset-top) + var(--dialog-extra-top-padding));
--dialog-outer-padding-bottom: 0px;
--dialog-inner-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-inner-vert)));
}
.mobile-tag-list:not(.floating) .dialog {
border-radius: var(--border-radius-large);
overflow: hidden;
}
.mobile-tag-list > .dialog {
padding: var(--dialog-inner-padding-top) var(--dialog-inner-padding-right) var(--dialog-inner-padding-bottom) var(--dialog-inner-padding-left);
transform: translateY(calc((var(--dialog-visible) - 0) * -100% / 1 + 100%));
}
.mobile-tag-list > .dialog .scroll {
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
margin-right: calc(var(--dialog-inner-padding-right) * -1);
padding-right: calc(var(--dialog-inner-padding-right));
margin-left: calc(var(--dialog-inner-padding-left) * -1);
padding-left: calc(var(--dialog-inner-padding-left));
margin-bottom: calc(var(--dialog-inner-padding-bottom) * -1);
padding-bottom: calc(var(--dialog-inner-padding-bottom));
}
.mobile-tag-list.floating .dialog {
min-width: 20em;
max-width: min(45em, 90vw);
max-height: 90vh;
border-radius: var(--border-radius-large);
}
.mobile-tag-list.small .dialog {
min-width: 10em;
max-width: 45em;
}
.mobile-tag-list:not(.floating) .dialog {
width: 100%;
}
.mobile-tag-list .dialog {
max-height: 100%;
border: solid 1px var(--ui-border-color);
border-radius: var(--border-radius-large);
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--ui-fg-color);
position: relative;
display: flex;
flex-direction: column;
}
.mobile-tag-list.dragging-dialog > .dialog {
pointer-events: none;
}
.mobile-tag-list.dragging-dialog .vertical-scroller {
overflow: hidden !important;
}
.mobile-tag-list .header {
--icon-size: 2;
line-height: 2em;
display: flex;
align-items: center;
}
.mobile-tag-list .header .close-button-container {
flex: 1;
}
.mobile-tag-list .header .close-button-container .close-button {
--icon-size: 1.5;
color: var(--button-color);
cursor: pointer;
justify-self: center;
}
@media (hover: hover) {
.mobile-tag-list .header .close-button-container .close-button:hover {
color: var(--button-highlight-color);
}
}
.mobile-tag-list .header .close-button-container .close-button > svg {
display: block;
}
.mobile-tag-list .header .header-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.mobile-tag-list .header .center-header-helper {
width: calc(1em * var(--icon-size) + 1em);
flex: 1;
flex-shrink: 100000;
}
.mobile-tag-list > .dialog {
padding-bottom: 0;
}
.mobile-tag-list > .dialog > .scroll {
margin-bottom: 0;
padding-bottom: 0;
}
.mobile-tag-list .popup-bookmark-tag-entry {
font-size: 120%;
min-width: 20em;
}
.mobile-tag-list .menu-bar {
--icon-size: 1.6;
position: sticky;
bottom: 0;
display: flex;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
padding: 8px 0;
padding-bottom: var(--dialog-inner-padding-bottom);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.text-entry-popup {
position: fixed;
z-index: 1000;
inset: 0;
overscroll-behavior: contain;
--dialog-visible: 1;
display: flex;
align-items: flex-end;
justify-content: center;
--dialog-bg-color: rgba(0, 0, 0, var(--dialog-bg-alpha));
background-color: rgba(0, 0, 0, min(var(--dialog-backdrop-alpha), max(0, (var(--dialog-visible) - 0) * (var(--dialog-backdrop-alpha) - 0) / 1 + 0)));
--edge-padding-inner-horiz: 20px;
--edge-padding-inner-vert: 20px;
--edge-padding-outer-horiz: 40px;
--edge-padding-outer-vert: 20px;
padding: var(--dialog-outer-padding-top) var(--dialog-outer-padding-right) var(--dialog-outer-padding-bottom) var(--dialog-outer-padding-left);
}
html.ios .text-entry-popup {
inset: -1px 0;
}
.text-entry-popup.floating {
align-items: center;
}
.text-entry-popup:not(.floating) {
--dialog-bg-alpha: 0.5;
--dialog-backdrop-alpha: 0.5;
}
.text-entry-popup.floating {
--dialog-bg-alpha: 0.75;
--dialog-backdrop-alpha: 0.5;
}
html.mobile .text-entry-popup {
font-size: 1.2rem;
}
html.mobile .text-entry-popup {
--edge-padding-inner-horiz: 10px;
--edge-padding-inner-vert: 10px;
}
.text-entry-popup.floating {
--dialog-outer-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-top: calc(max(env(safe-area-inset-top), var(--edge-padding-outer-vert)));
--dialog-outer-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-outer-vert)));
--dialog-inner-padding-left: var(--edge-padding-inner-horiz);
--dialog-inner-padding-right: var(--edge-padding-inner-horiz);
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: var(--edge-padding-inner-vert);
}
.text-entry-popup.floating .dialog {
opacity: min(1, max(0, (var(--dialog-visible) - 0) * 1 / 1 + 0));
}
.text-entry-popup:not(.floating) {
--edge-padding-horiz: 20px;
--edge-padding-vert: 20px;
--dialog-extra-top-padding: 2vh;
--dialog-outer-padding-left: 0px;
--dialog-outer-padding-right: 0px;
--dialog-outer-padding-top: calc(env(safe-area-inset-top) + var(--dialog-extra-top-padding));
--dialog-outer-padding-bottom: 0px;
--dialog-inner-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-inner-vert)));
}
.text-entry-popup:not(.floating) .dialog {
border-radius: var(--border-radius-large);
overflow: hidden;
}
.text-entry-popup > .dialog {
padding: var(--dialog-inner-padding-top) var(--dialog-inner-padding-right) var(--dialog-inner-padding-bottom) var(--dialog-inner-padding-left);
transform: translateY(calc((var(--dialog-visible) - 0) * -100% / 1 + 100%));
}
.text-entry-popup > .dialog .scroll {
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
margin-right: calc(var(--dialog-inner-padding-right) * -1);
padding-right: calc(var(--dialog-inner-padding-right));
margin-left: calc(var(--dialog-inner-padding-left) * -1);
padding-left: calc(var(--dialog-inner-padding-left));
margin-bottom: calc(var(--dialog-inner-padding-bottom) * -1);
padding-bottom: calc(var(--dialog-inner-padding-bottom));
}
.text-entry-popup.floating .dialog {
min-width: 20em;
max-width: min(45em, 90vw);
max-height: 90vh;
border-radius: var(--border-radius-large);
}
.text-entry-popup.small .dialog {
min-width: 10em;
max-width: 45em;
}
.text-entry-popup:not(.floating) .dialog {
width: 100%;
}
.text-entry-popup .dialog {
max-height: 100%;
border: solid 1px var(--ui-border-color);
border-radius: var(--border-radius-large);
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--ui-fg-color);
position: relative;
display: flex;
flex-direction: column;
}
.text-entry-popup.dragging-dialog > .dialog {
pointer-events: none;
}
.text-entry-popup.dragging-dialog .vertical-scroller {
overflow: hidden !important;
}
.text-entry-popup .header {
--icon-size: 2;
line-height: 2em;
display: flex;
align-items: center;
}
.text-entry-popup .header .close-button-container {
flex: 1;
}
.text-entry-popup .header .close-button-container .close-button {
--icon-size: 1.5;
color: var(--button-color);
cursor: pointer;
justify-self: center;
}
@media (hover: hover) {
.text-entry-popup .header .close-button-container .close-button:hover {
color: var(--button-highlight-color);
}
}
.text-entry-popup .header .close-button-container .close-button > svg {
display: block;
}
.text-entry-popup .header .header-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.text-entry-popup .header .center-header-helper {
width: calc(1em * var(--icon-size) + 1em);
flex: 1;
flex-shrink: 100000;
}
.text-entry-popup .header {
font-size: 20px;
}
.text-entry-popup .input-box {
position: relative;
display: flex;
align-items: center;
}
.text-entry-popup .input-box > .editor {
flex: 1;
padding: 4px;
min-width: min(50vw, 25em);
white-space: pre-wrap;
}
.text-entry-popup .input-box > .submit-button {
cursor: pointer;
display: flex;
margin-left: 6px;
padding: 0.25em;
border: 1px solid white;
}
@media (hover: hover) {
.text-entry-popup .input-box > .submit-button:hover {
background-color: #444;
}
}
.confirm-dialog {
position: fixed;
z-index: 1000;
inset: 0;
overscroll-behavior: contain;
--dialog-visible: 1;
display: flex;
align-items: flex-end;
justify-content: center;
--dialog-bg-color: rgba(0, 0, 0, var(--dialog-bg-alpha));
background-color: rgba(0, 0, 0, min(var(--dialog-backdrop-alpha), max(0, (var(--dialog-visible) - 0) * (var(--dialog-backdrop-alpha) - 0) / 1 + 0)));
--edge-padding-inner-horiz: 20px;
--edge-padding-inner-vert: 20px;
--edge-padding-outer-horiz: 40px;
--edge-padding-outer-vert: 20px;
padding: var(--dialog-outer-padding-top) var(--dialog-outer-padding-right) var(--dialog-outer-padding-bottom) var(--dialog-outer-padding-left);
}
html.ios .confirm-dialog {
inset: -1px 0;
}
.confirm-dialog.floating {
align-items: center;
}
.confirm-dialog:not(.floating) {
--dialog-bg-alpha: 0.5;
--dialog-backdrop-alpha: 0.5;
}
.confirm-dialog.floating {
--dialog-bg-alpha: 0.75;
--dialog-backdrop-alpha: 0.5;
}
html.mobile .confirm-dialog {
font-size: 1.2rem;
}
html.mobile .confirm-dialog {
--edge-padding-inner-horiz: 10px;
--edge-padding-inner-vert: 10px;
}
.confirm-dialog.floating {
--dialog-outer-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-top: calc(max(env(safe-area-inset-top), var(--edge-padding-outer-vert)));
--dialog-outer-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-outer-vert)));
--dialog-inner-padding-left: var(--edge-padding-inner-horiz);
--dialog-inner-padding-right: var(--edge-padding-inner-horiz);
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: var(--edge-padding-inner-vert);
}
.confirm-dialog.floating .dialog {
opacity: min(1, max(0, (var(--dialog-visible) - 0) * 1 / 1 + 0));
}
.confirm-dialog:not(.floating) {
--edge-padding-horiz: 20px;
--edge-padding-vert: 20px;
--dialog-extra-top-padding: 2vh;
--dialog-outer-padding-left: 0px;
--dialog-outer-padding-right: 0px;
--dialog-outer-padding-top: calc(env(safe-area-inset-top) + var(--dialog-extra-top-padding));
--dialog-outer-padding-bottom: 0px;
--dialog-inner-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-inner-vert)));
}
.confirm-dialog:not(.floating) .dialog {
border-radius: var(--border-radius-large);
overflow: hidden;
}
.confirm-dialog > .dialog {
padding: var(--dialog-inner-padding-top) var(--dialog-inner-padding-right) var(--dialog-inner-padding-bottom) var(--dialog-inner-padding-left);
transform: translateY(calc((var(--dialog-visible) - 0) * -100% / 1 + 100%));
}
.confirm-dialog > .dialog .scroll {
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
margin-right: calc(var(--dialog-inner-padding-right) * -1);
padding-right: calc(var(--dialog-inner-padding-right));
margin-left: calc(var(--dialog-inner-padding-left) * -1);
padding-left: calc(var(--dialog-inner-padding-left));
margin-bottom: calc(var(--dialog-inner-padding-bottom) * -1);
padding-bottom: calc(var(--dialog-inner-padding-bottom));
}
.confirm-dialog.floating .dialog {
min-width: 20em;
max-width: min(45em, 90vw);
max-height: 90vh;
border-radius: var(--border-radius-large);
}
.confirm-dialog.small .dialog {
min-width: 10em;
max-width: 45em;
}
.confirm-dialog:not(.floating) .dialog {
width: 100%;
}
.confirm-dialog .dialog {
max-height: 100%;
border: solid 1px var(--ui-border-color);
border-radius: var(--border-radius-large);
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--ui-fg-color);
position: relative;
display: flex;
flex-direction: column;
}
.confirm-dialog.dragging-dialog > .dialog {
pointer-events: none;
}
.confirm-dialog.dragging-dialog .vertical-scroller {
overflow: hidden !important;
}
.confirm-dialog .header {
--icon-size: 2;
line-height: 2em;
display: flex;
align-items: center;
}
.confirm-dialog .header .close-button-container {
flex: 1;
}
.confirm-dialog .header .close-button-container .close-button {
--icon-size: 1.5;
color: var(--button-color);
cursor: pointer;
justify-self: center;
}
@media (hover: hover) {
.confirm-dialog .header .close-button-container .close-button:hover {
color: var(--button-highlight-color);
}
}
.confirm-dialog .header .close-button-container .close-button > svg {
display: block;
}
.confirm-dialog .header .header-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.confirm-dialog .header .center-header-helper {
width: calc(1em * var(--icon-size) + 1em);
flex: 1;
flex-shrink: 100000;
}
.confirm-dialog .scroll {
display: flex;
flex-direction: column;
align-items: center;
}
.confirm-dialog .scroll .text {
text-align: center;
padding: 1em 0;
}
.confirm-dialog .scroll .input-box {
display: flex;
align-items: center;
gap: 2em;
}
.screen-illust-container {
position: fixed;
inset: 0;
z-index: 1;
--illust-hidden: 1;
}
.screen-illust-container .view-container {
user-select: none;
-webkit-user-select: none;
cursor: pointer;
translate: 0px calc(var(--menu-bar-pos) * var(--menu-bar-height) * -1);
}
.screen-illust-container .view-container .viewer {
position: absolute;
inset: var(--fullscreen-top) var(--fullscreen-right) var(--fullscreen-bottom) var(--fullscreen-left);
}
.screen-illust-container .view-container .viewer.hidden-widget {
display: unset;
visibility: hidden;
pointer-events: none;
}
.screen-illust-container .fade-search {
position: fixed;
z-index: -1;
inset: -100vh;
pointer-events: none;
background-color: rgba(0, 0, 0, calc((var(--illust-hidden) - 0.1) * -1 / 0.9 + 1));
}
.screen-illust-container .view-container {
position: absolute;
overflow: hidden;
inset: 0;
transform: translateX(calc(var(--illust-hidden) * var(--animation-x))) translateY(calc(var(--illust-hidden) * var(--animation-y))) scale(calc((var(--illust-hidden) - 0) * (var(--animation-scale) - 1) / 1 + 1));
transform-origin: 0% 0%;
opacity: calc((var(--illust-hidden) - 0) * -1 / 1 + 1);
}
html.mobile .screen-illust-container .view-container .viewer .rounded-box {
overflow: hidden;
--rounding-amount: 1;
border-radius: var(--border-radius-large);
will-change: transform;
}
.screen-illust-container .manga-page-indicator {
position: absolute;
bottom: 0px;
--width: min(50%, 400px);
left: calc((100% - var(--width)) / 2);
right: calc((100% - var(--width)) / 2);
height: 10px;
z-index: 1;
pointer-events: none;
box-sizing: content-box;
border: 1px solid #000;
border-bottom: none;
border-radius: var(--border-radius-large);
overflow: hidden;
opacity: 0;
background-color: rgba(255, 255, 255, 0.25);
}
.screen-illust-container .manga-page-indicator .bar {
position: absolute;
inset: 0;
background: linear-gradient(to right, rgba(255, 20, 20, 0.75) var(--percent), rgba(50, 50, 50, 0) var(--percent));
}
.screen-illust-container .page-change-indicator {
position: absolute;
height: 100%;
display: flex;
align-items: center;
pointer-events: none;
/* Hide the | portion of >| when showing last page rather than end of results. */
}
.screen-illust-container .page-change-indicator[data-side=left] {
margin-left: 20px;
left: 0;
}
.screen-illust-container .page-change-indicator[data-side=right] {
margin-right: 20px;
right: 0;
}
.screen-illust-container .page-change-indicator[data-side=right] svg {
transform-origin: center center;
transform: scale(-1, 1);
}
.screen-illust-container .page-change-indicator[data-icon=last-page] svg .bar {
display: none;
}
.screen-illust-container .page-change-indicator svg {
opacity: 0;
}
.screen-illust-container .page-change-indicator.flash svg {
animation: flash-page-change-opacity 400ms ease-out 1 forwards;
}
.screen-illust-container .page-change-indicator.flash svg .animated {
animation: flash-page-change-part 300ms ease-out 1 forwards;
}
@keyframes flash-page-change-opacity {
0% {
opacity: 1;
}
40% {
opacity: 1;
}
80% {
opacity: 0;
}
}
@keyframes flash-page-change-part {
0% {
transform: translate(0, 0px);
}
20% {
transform: translate(-4px, 0px);
}
100% {
transform: translate(0, 0px);
}
}
.screen-illust-container .translation-status {
position: absolute;
left: 2rem;
bottom: 2rem;
pointer-events: none;
transition: opacity 0.25s;
opacity: 0;
}
html[data-loading-translation] .screen-illust-container .translation-status {
opacity: 1;
}
.screen-illust-container .translation-status .translation-contents {
display: flex;
align-items: center;
justify-content: center;
padding: 0.5rem;
font-size: 2rem;
background-color: #4040a0;
border-radius: 100%;
}
.link-tab-popup .explanation {
max-width: 25em;
width: 100%;
text-align: center;
margin: 0 auto;
}
.link-tab-popup .button {
display: inline-block;
cursor: pointer;
background-color: #000;
padding: 0.5em 1em;
margin: 0.5em;
border-radius: var(--border-radius-large);
}
.link-tab-popup .content {
width: 400px;
padding: 1em;
}
.link-tab-popup .buttons {
display: flex;
}
.link-tab-popup .tutorial-monitor {
width: 290px;
height: 125px;
margin-bottom: -20px;
}
.link-tab-popup .tutorial-monitor .rotating-monitor {
transform-origin: 75px 30px;
animation: rotate-monitor 4500ms linear infinite;
}
@keyframes rotate-monitor {
0% {
transform: rotate(0deg);
}
10% {
transform: rotate(90deg);
}
50% {
transform: rotate(90deg);
}
60% {
transform: rotate(0deg);
}
}
.years-ago {
padding: 0.25em;
margin: 0.25em;
white-space: nowrap;
/* These links are mostly the same as box-link, but since the
* menu background is the same as the box-link background color,
* shift it a little to make it clear these are buttons. */
}
.years-ago > a {
padding: 4px 10px;
background-color: #444;
}
.tree {
user-select: none;
-webkit-user-select: none;
overflow-x: hidden;
overflow-y: auto;
flex: 1;
}
.tree .tree-item {
position: relative;
contain-intrinsic-height: 32px;
}
.tree .tree-item.allow-content-visibility {
content-visibility: auto;
}
.tree .tree-item:not(.root-item) > .items {
margin-left: 1em;
}
.tree .tree-item.selected > .self > .label {
background-color: #003088;
}
.tree .tree-item > .self {
display: flex;
flex-direction: row;
align-items: center;
height: 2em;
}
.tree .tree-item > .self:focus {
outline: none;
}
.tree .tree-item > .self > .label {
padding: 0.5em;
white-space: nowrap;
}
.tree .tree-item > .self.root-item {
display: none;
}
.tree .tree-item > .self > .expander {
display: flex;
justify-content: center;
align-items: center;
font-size: 50%;
width: 3em;
height: 100%;
}
.tree .tree-item > .self > .expander > .expander-button {
display: none;
width: 3em;
text-align: center;
vertical-align: middle;
}
.tree .tree-item > .self > .expander[data-mode=loading] > .loading {
display: block;
}
.tree .tree-item > .self > .expander[data-mode=none] > .none {
display: block;
}
.tree .tree-item > .self > .expander[data-pending=true] > .expand {
opacity: 0.5;
}
.tree .tree-item > .self > .expander[data-mode=expandable] > .expand, .tree .tree-item > .self > .expander[data-mode=expanded] > .expand {
display: block;
}
.tree .tree-item > .self > .expander .expand {
transform: rotate(0deg);
transition: transform 0.25s;
}
.tree .tree-item > .self > .expander[data-mode=expanded] > .expand {
transform: rotate(90deg);
}
.screen-search-container {
--navigation-box-width: 25%;
--navigation-box-reserved-width: var(--navigation-box-width);
height: 100%;
}
.screen-search-container:not([data-show-navigation]) {
--navigation-box-reserved-width: 0%;
}
.local-navigation-box {
height: 100vh;
width: var(--navigation-box-width);
position: fixed;
top: 0;
left: 0;
background-color: #111;
border-right: solid 1px #444;
padding-top: 0.5em;
padding-left: 0.5em;
opacity: 1;
transition: opacity 0.35s, transform 0.35s;
display: flex;
flex-direction: column;
}
.screen-search-container:not([data-show-navigation]) .local-navigation-box {
opacity: 0;
pointer-events: none;
transform: translate(-50%, 0);
}
.tree-popup {
background-color: #222;
color: #fff;
position: fixed;
pointer-events: none;
outline-style: dotted;
outline-width: 1px;
outline-color: #aaa;
}
.tree-popup > .label {
white-space: nowrap;
}
.thumb-popup {
position: fixed;
pointer-events: none;
margin-left: 10px;
width: 25%;
height: 40%;
max-height: 400px;
max-width: 400px;
}
.thumb-popup > img {
object-fit: contain;
width: 100%;
height: 100%;
}
.settings-dialog {
position: fixed;
z-index: 1000;
inset: 0;
overscroll-behavior: contain;
--dialog-visible: 1;
display: flex;
align-items: flex-end;
justify-content: center;
--dialog-bg-color: rgba(0, 0, 0, var(--dialog-bg-alpha));
background-color: rgba(0, 0, 0, min(var(--dialog-backdrop-alpha), max(0, (var(--dialog-visible) - 0) * (var(--dialog-backdrop-alpha) - 0) / 1 + 0)));
--edge-padding-inner-horiz: 20px;
--edge-padding-inner-vert: 20px;
--edge-padding-outer-horiz: 40px;
--edge-padding-outer-vert: 20px;
padding: var(--dialog-outer-padding-top) var(--dialog-outer-padding-right) var(--dialog-outer-padding-bottom) var(--dialog-outer-padding-left);
font-size: 1.15rem;
}
html.ios .settings-dialog {
inset: -1px 0;
}
.settings-dialog.floating {
align-items: center;
}
.settings-dialog:not(.floating) {
--dialog-bg-alpha: 0.5;
--dialog-backdrop-alpha: 0.5;
}
.settings-dialog.floating {
--dialog-bg-alpha: 0.75;
--dialog-backdrop-alpha: 0.5;
}
html.mobile .settings-dialog {
font-size: 1.2rem;
}
html.mobile .settings-dialog {
--edge-padding-inner-horiz: 10px;
--edge-padding-inner-vert: 10px;
}
.settings-dialog.floating {
--dialog-outer-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-top: calc(max(env(safe-area-inset-top), var(--edge-padding-outer-vert)));
--dialog-outer-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-outer-vert)));
--dialog-inner-padding-left: var(--edge-padding-inner-horiz);
--dialog-inner-padding-right: var(--edge-padding-inner-horiz);
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: var(--edge-padding-inner-vert);
}
.settings-dialog.floating .dialog {
opacity: min(1, max(0, (var(--dialog-visible) - 0) * 1 / 1 + 0));
}
.settings-dialog:not(.floating) {
--edge-padding-horiz: 20px;
--edge-padding-vert: 20px;
--dialog-extra-top-padding: 2vh;
--dialog-outer-padding-left: 0px;
--dialog-outer-padding-right: 0px;
--dialog-outer-padding-top: calc(env(safe-area-inset-top) + var(--dialog-extra-top-padding));
--dialog-outer-padding-bottom: 0px;
--dialog-inner-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-inner-vert)));
}
.settings-dialog:not(.floating) .dialog {
border-radius: var(--border-radius-large);
overflow: hidden;
}
.settings-dialog > .dialog {
padding: var(--dialog-inner-padding-top) var(--dialog-inner-padding-right) var(--dialog-inner-padding-bottom) var(--dialog-inner-padding-left);
transform: translateY(calc((var(--dialog-visible) - 0) * -100% / 1 + 100%));
}
.settings-dialog > .dialog .scroll {
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
margin-right: calc(var(--dialog-inner-padding-right) * -1);
padding-right: calc(var(--dialog-inner-padding-right));
margin-left: calc(var(--dialog-inner-padding-left) * -1);
padding-left: calc(var(--dialog-inner-padding-left));
margin-bottom: calc(var(--dialog-inner-padding-bottom) * -1);
padding-bottom: calc(var(--dialog-inner-padding-bottom));
}
.settings-dialog.floating .dialog {
min-width: 20em;
max-width: min(45em, 90vw);
max-height: 90vh;
border-radius: var(--border-radius-large);
}
.settings-dialog.small .dialog {
min-width: 10em;
max-width: 45em;
}
.settings-dialog:not(.floating) .dialog {
width: 100%;
}
.settings-dialog .dialog {
max-height: 100%;
border: solid 1px var(--ui-border-color);
border-radius: var(--border-radius-large);
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--ui-fg-color);
position: relative;
display: flex;
flex-direction: column;
}
.settings-dialog.dragging-dialog > .dialog {
pointer-events: none;
}
.settings-dialog.dragging-dialog .vertical-scroller {
overflow: hidden !important;
}
.settings-dialog .header {
--icon-size: 2;
line-height: 2em;
display: flex;
align-items: center;
}
.settings-dialog .header .close-button-container {
flex: 1;
}
.settings-dialog .header .close-button-container .close-button {
--icon-size: 1.5;
color: var(--button-color);
cursor: pointer;
justify-self: center;
}
@media (hover: hover) {
.settings-dialog .header .close-button-container .close-button:hover {
color: var(--button-highlight-color);
}
}
.settings-dialog .header .close-button-container .close-button > svg {
display: block;
}
.settings-dialog .header .header-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.settings-dialog .header .center-header-helper {
width: calc(1em * var(--icon-size) + 1em);
flex: 1;
flex-shrink: 100000;
}
.settings-dialog.floating .dialog {
min-height: min(30em, 100%);
width: 100%;
}
.settings-dialog.phone .scroll {
justify-content: center;
}
.settings-dialog .sections {
white-space: nowrap;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.settings-dialog .sections > .box-link {
padding: 0.5em;
cursor: pointer;
}
.settings-dialog .sections > .box-link:not(.selected) {
opacity: 0.65;
}
.settings-dialog .sections > .box-link:not(.active) {
background: none;
}
@media (hover: hover) {
.settings-dialog .sections > .box-link:hover {
background-color: var(--box-link-hover-color);
}
}
html[data-whats-new-updated] .settings-dialog .sections .settings-page-button[data-page=whatsNew] {
color: #ff0;
}
.settings-dialog > .dialog > .scroll > .items {
flex: 1;
overflow-y: auto;
}
.settings-dialog .settings-list {
display: flex;
flex-direction: column;
height: 100%;
}
.settings-dialog .settings-list .settings-row {
padding: 0.5em;
}
.settings-dialog .settings-list .box-link {
height: auto;
align-items: stretch;
flex-direction: column;
--box-link-bg-color: #00000000;
}
.settings-dialog .settings-list .box-link > .buttons > .box-link {
padding: 0.35em 0.75em;
}
.settings-dialog .settings-list .box-link:not(.clickable):hover {
cursor: inherit;
background: none;
}
.settings-dialog .settings-list > .box-link > .label-box .label {
flex: 1;
}
.settings-dialog .settings-list > .box-link > .explanation {
font-size: 80%;
color: #ccc;
}
html.mobile .settings-dialog .sections {
gap: 0.5em;
}
html.mobile .settings-dialog:not(.floating) .sections {
flex: 1;
}
.settings-dialog > .dialog > .scroll {
display: flex;
flex-direction: row;
gap: 2em;
}
.settings-dialog-page {
position: fixed;
z-index: 1000;
inset: 0;
overscroll-behavior: contain;
--dialog-visible: 1;
display: flex;
align-items: flex-end;
justify-content: center;
--dialog-bg-color: rgba(0, 0, 0, var(--dialog-bg-alpha));
background-color: rgba(0, 0, 0, min(var(--dialog-backdrop-alpha), max(0, (var(--dialog-visible) - 0) * (var(--dialog-backdrop-alpha) - 0) / 1 + 0)));
--edge-padding-inner-horiz: 20px;
--edge-padding-inner-vert: 20px;
--edge-padding-outer-horiz: 40px;
--edge-padding-outer-vert: 20px;
padding: var(--dialog-outer-padding-top) var(--dialog-outer-padding-right) var(--dialog-outer-padding-bottom) var(--dialog-outer-padding-left);
font-size: 1.15rem;
}
html.ios .settings-dialog-page {
inset: -1px 0;
}
.settings-dialog-page.floating {
align-items: center;
}
.settings-dialog-page:not(.floating) {
--dialog-bg-alpha: 0.5;
--dialog-backdrop-alpha: 0.5;
}
.settings-dialog-page.floating {
--dialog-bg-alpha: 0.75;
--dialog-backdrop-alpha: 0.5;
}
html.mobile .settings-dialog-page {
font-size: 1.2rem;
}
html.mobile .settings-dialog-page {
--edge-padding-inner-horiz: 10px;
--edge-padding-inner-vert: 10px;
}
.settings-dialog-page.floating {
--dialog-outer-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-top: calc(max(env(safe-area-inset-top), var(--edge-padding-outer-vert)));
--dialog-outer-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-outer-vert)));
--dialog-inner-padding-left: var(--edge-padding-inner-horiz);
--dialog-inner-padding-right: var(--edge-padding-inner-horiz);
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: var(--edge-padding-inner-vert);
}
.settings-dialog-page.floating .dialog {
opacity: min(1, max(0, (var(--dialog-visible) - 0) * 1 / 1 + 0));
}
.settings-dialog-page:not(.floating) {
--edge-padding-horiz: 20px;
--edge-padding-vert: 20px;
--dialog-extra-top-padding: 2vh;
--dialog-outer-padding-left: 0px;
--dialog-outer-padding-right: 0px;
--dialog-outer-padding-top: calc(env(safe-area-inset-top) + var(--dialog-extra-top-padding));
--dialog-outer-padding-bottom: 0px;
--dialog-inner-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-inner-vert)));
}
.settings-dialog-page:not(.floating) .dialog {
border-radius: var(--border-radius-large);
overflow: hidden;
}
.settings-dialog-page > .dialog {
padding: var(--dialog-inner-padding-top) var(--dialog-inner-padding-right) var(--dialog-inner-padding-bottom) var(--dialog-inner-padding-left);
transform: translateY(calc((var(--dialog-visible) - 0) * -100% / 1 + 100%));
}
.settings-dialog-page > .dialog .scroll {
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
margin-right: calc(var(--dialog-inner-padding-right) * -1);
padding-right: calc(var(--dialog-inner-padding-right));
margin-left: calc(var(--dialog-inner-padding-left) * -1);
padding-left: calc(var(--dialog-inner-padding-left));
margin-bottom: calc(var(--dialog-inner-padding-bottom) * -1);
padding-bottom: calc(var(--dialog-inner-padding-bottom));
}
.settings-dialog-page.floating .dialog {
min-width: 20em;
max-width: min(45em, 90vw);
max-height: 90vh;
border-radius: var(--border-radius-large);
}
.settings-dialog-page.small .dialog {
min-width: 10em;
max-width: 45em;
}
.settings-dialog-page:not(.floating) .dialog {
width: 100%;
}
.settings-dialog-page .dialog {
max-height: 100%;
border: solid 1px var(--ui-border-color);
border-radius: var(--border-radius-large);
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--ui-fg-color);
position: relative;
display: flex;
flex-direction: column;
}
.settings-dialog-page.dragging-dialog > .dialog {
pointer-events: none;
}
.settings-dialog-page.dragging-dialog .vertical-scroller {
overflow: hidden !important;
}
.settings-dialog-page .header {
--icon-size: 2;
line-height: 2em;
display: flex;
align-items: center;
}
.settings-dialog-page .header .close-button-container {
flex: 1;
}
.settings-dialog-page .header .close-button-container .close-button {
--icon-size: 1.5;
color: var(--button-color);
cursor: pointer;
justify-self: center;
}
@media (hover: hover) {
.settings-dialog-page .header .close-button-container .close-button:hover {
color: var(--button-highlight-color);
}
}
.settings-dialog-page .header .close-button-container .close-button > svg {
display: block;
}
.settings-dialog-page .header .header-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.settings-dialog-page .header .center-header-helper {
width: calc(1em * var(--icon-size) + 1em);
flex: 1;
flex-shrink: 100000;
}
.settings-dialog-page.floating .dialog {
min-height: min(30em, 100%);
width: 100%;
}
.settings-dialog-page.phone .scroll {
justify-content: center;
}
.settings-dialog-page .sections {
white-space: nowrap;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.settings-dialog-page .sections > .box-link {
padding: 0.5em;
cursor: pointer;
}
.settings-dialog-page .sections > .box-link:not(.selected) {
opacity: 0.65;
}
.settings-dialog-page .sections > .box-link:not(.active) {
background: none;
}
@media (hover: hover) {
.settings-dialog-page .sections > .box-link:hover {
background-color: var(--box-link-hover-color);
}
}
html[data-whats-new-updated] .settings-dialog-page .sections .settings-page-button[data-page=whatsNew] {
color: #ff0;
}
.settings-dialog-page > .dialog > .scroll > .items {
flex: 1;
overflow-y: auto;
}
.settings-dialog-page .settings-list {
display: flex;
flex-direction: column;
height: 100%;
}
.settings-dialog-page .settings-list .settings-row {
padding: 0.5em;
}
.settings-dialog-page .settings-list .box-link {
height: auto;
align-items: stretch;
flex-direction: column;
--box-link-bg-color: #00000000;
}
.settings-dialog-page .settings-list .box-link > .buttons > .box-link {
padding: 0.35em 0.75em;
}
.settings-dialog-page .settings-list .box-link:not(.clickable):hover {
cursor: inherit;
background: none;
}
.settings-dialog-page .settings-list > .box-link > .label-box .label {
flex: 1;
}
.settings-dialog-page .settings-list > .box-link > .explanation {
font-size: 80%;
color: #ccc;
}
.edit-search-dialog {
position: fixed;
z-index: 1000;
inset: 0;
overscroll-behavior: contain;
--dialog-visible: 1;
display: flex;
align-items: flex-end;
justify-content: center;
--dialog-bg-color: rgba(0, 0, 0, var(--dialog-bg-alpha));
background-color: rgba(0, 0, 0, min(var(--dialog-backdrop-alpha), max(0, (var(--dialog-visible) - 0) * (var(--dialog-backdrop-alpha) - 0) / 1 + 0)));
--edge-padding-inner-horiz: 20px;
--edge-padding-inner-vert: 20px;
--edge-padding-outer-horiz: 40px;
--edge-padding-outer-vert: 20px;
padding: var(--dialog-outer-padding-top) var(--dialog-outer-padding-right) var(--dialog-outer-padding-bottom) var(--dialog-outer-padding-left);
}
html.ios .edit-search-dialog {
inset: -1px 0;
}
.edit-search-dialog.floating {
align-items: center;
}
.edit-search-dialog:not(.floating) {
--dialog-bg-alpha: 0.5;
--dialog-backdrop-alpha: 0.5;
}
.edit-search-dialog.floating {
--dialog-bg-alpha: 0.75;
--dialog-backdrop-alpha: 0.5;
}
html.mobile .edit-search-dialog {
font-size: 1.2rem;
}
html.mobile .edit-search-dialog {
--edge-padding-inner-horiz: 10px;
--edge-padding-inner-vert: 10px;
}
.edit-search-dialog.floating {
--dialog-outer-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-outer-horiz)));
--dialog-outer-padding-top: calc(max(env(safe-area-inset-top), var(--edge-padding-outer-vert)));
--dialog-outer-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-outer-vert)));
--dialog-inner-padding-left: var(--edge-padding-inner-horiz);
--dialog-inner-padding-right: var(--edge-padding-inner-horiz);
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: var(--edge-padding-inner-vert);
}
.edit-search-dialog.floating .dialog {
opacity: min(1, max(0, (var(--dialog-visible) - 0) * 1 / 1 + 0));
}
.edit-search-dialog:not(.floating) {
--edge-padding-horiz: 20px;
--edge-padding-vert: 20px;
--dialog-extra-top-padding: 2vh;
--dialog-outer-padding-left: 0px;
--dialog-outer-padding-right: 0px;
--dialog-outer-padding-top: calc(env(safe-area-inset-top) + var(--dialog-extra-top-padding));
--dialog-outer-padding-bottom: 0px;
--dialog-inner-padding-left: calc(max(env(safe-area-inset-left), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-right: calc(max(env(safe-area-inset-right), var(--edge-padding-inner-horiz)));
--dialog-inner-padding-top: var(--edge-padding-inner-vert);
--dialog-inner-padding-bottom: calc(max(var(--fixed-safe-area-inset-bottom), var(--edge-padding-inner-vert)));
}
.edit-search-dialog:not(.floating) .dialog {
border-radius: var(--border-radius-large);
overflow: hidden;
}
.edit-search-dialog > .dialog {
padding: var(--dialog-inner-padding-top) var(--dialog-inner-padding-right) var(--dialog-inner-padding-bottom) var(--dialog-inner-padding-left);
transform: translateY(calc((var(--dialog-visible) - 0) * -100% / 1 + 100%));
}
.edit-search-dialog > .dialog .scroll {
overflow-x: hidden;
overflow-y: auto;
touch-action: pan-y;
margin-right: calc(var(--dialog-inner-padding-right) * -1);
padding-right: calc(var(--dialog-inner-padding-right));
margin-left: calc(var(--dialog-inner-padding-left) * -1);
padding-left: calc(var(--dialog-inner-padding-left));
margin-bottom: calc(var(--dialog-inner-padding-bottom) * -1);
padding-bottom: calc(var(--dialog-inner-padding-bottom));
}
.edit-search-dialog.floating .dialog {
min-width: 20em;
max-width: min(45em, 90vw);
max-height: 90vh;
border-radius: var(--border-radius-large);
}
.edit-search-dialog.small .dialog {
min-width: 10em;
max-width: 45em;
}
.edit-search-dialog:not(.floating) .dialog {
width: 100%;
}
.edit-search-dialog .dialog {
max-height: 100%;
border: solid 1px var(--ui-border-color);
border-radius: var(--border-radius-large);
background-color: var(--ui-bg-color);
backdrop-filter: var(--ui-bg-blur);
color: var(--ui-fg-color);
position: relative;
display: flex;
flex-direction: column;
}
.edit-search-dialog.dragging-dialog > .dialog {
pointer-events: none;
}
.edit-search-dialog.dragging-dialog .vertical-scroller {
overflow: hidden !important;
}
.edit-search-dialog .header {
--icon-size: 2;
line-height: 2em;
display: flex;
align-items: center;
}
.edit-search-dialog .header .close-button-container {
flex: 1;
}
.edit-search-dialog .header .close-button-container .close-button {
--icon-size: 1.5;
color: var(--button-color);
cursor: pointer;
justify-self: center;
}
@media (hover: hover) {
.edit-search-dialog .header .close-button-container .close-button:hover {
color: var(--button-highlight-color);
}
}
.edit-search-dialog .header .close-button-container .close-button > svg {
display: block;
}
.edit-search-dialog .header .header-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-weight: 600;
}
.edit-search-dialog .header .center-header-helper {
width: calc(1em * var(--icon-size) + 1em);
flex: 1;
flex-shrink: 100000;
}
.edit-search-dialog .scroll {
display: flex;
flex-direction: column;
height: 100%;
}
.edit-search-dialog .scroll .navigation-button {
font-size: 1.25em;
--box-link-bg-color: none;
overflow: hidden;
}
.muted-tags-popup {
padding: 0.5em 1em;
display: flex;
flex-direction: column;
gap: 0.5em;
overflow-y: auto;
}
.edit-post-mute-dialog .mute-warning,
.muted-tags-popup .mute-warning {
border: solid 2px black;
border-radius: var(--border-radius-large);
background-color: #000;
padding: 1em;
}
.edit-post-mute-dialog .add-muted-user-box .font-icon,
.muted-tags-popup .add-muted-user-box .font-icon {
font-size: 24px;
vertical-align: middle;
}
.edit-post-mute-dialog .non-premium-mute-warning,
.muted-tags-popup .non-premium-mute-warning {
margin-right: 40px;
}
.edit-post-mute-dialog .non-premium-mute-warning .icon,
.muted-tags-popup .non-premium-mute-warning .icon {
font-size: 24px;
color: #ffff00;
}
.edit-post-mute-dialog .post-mute-list,
.muted-tags-popup .post-mute-list {
display: flex;
flex-direction: column;
gap: 4px;
}
.edit-post-mute-dialog .post-mute-list .entry,
.muted-tags-popup .post-mute-list .entry {
display: flex;
align-items: center;
gap: 0.5em;
}
.edit-post-mute-dialog .post-mute-list .entry.muted .tag-name,
.muted-tags-popup .post-mute-list .entry.muted .tag-name {
color: #ffaaaa;
}
.edit-post-mute-dialog .mute-list .remove-mute .font-icon,
.muted-tags-popup .mute-list .remove-mute .font-icon {
font-size: 24px;
vertical-align: middle;
}
.image-editor {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
pointer-events: none;
}
.image-editor.temporarily-hidden {
display: none;
pointer-events: none;
}
.image-editor .save-edits.dirty {
color: #0f0;
}
.image-editor .spinner .icon {
animation: spin 1000ms linear infinite forwards;
}
.image-editor .image-editor-buttons {
position: absolute;
display: grid;
grid-template-columns: 1fr auto 1fr;
font-size: 150%;
width: 100%;
align-items: flex-start;
}
body:not(.focused) .image-editor .image-editor-buttons.top {
display: none;
}
.image-editor .image-editor-buttons.top {
top: 0.5em;
}
.image-editor .image-editor-buttons.bottom {
bottom: 0.5em;
}
.image-editor .image-editor-buttons > .left {
margin-right: auto;
}
.image-editor .image-editor-buttons > .center {
grid-column-start: 2;
}
.image-editor .image-editor-buttons > .right {
margin-left: auto;
}
.image-editor .image-editor-buttons .image-editor-button-row {
pointer-events: auto;
}
.image-editor .image-editor-buttons .block-button .font-icon {
display: block;
}
@keyframes spin {
0% {
transform: rotate(0);
}
100% {
transform: rotate(360deg);
}
}
.crop-editor-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.crop-editor-overlay .crop-box {
position: relative;
--overlap: 1vh;
}
.crop-editor-overlay .crop-box [data-crop=all] {
box-shadow: 0px 0px 0px 10000px rgba(0, 0, 0, 0.5019607843);
}
.crop-editor-overlay .crop-box[data-mode=crop] [data-crop=all] {
outline: 3px solid #fff;
outline-style: ridge;
}
.crop-editor-overlay .crop-box[data-mode=safe_zone] [data-crop=all] {
outline: 1px solid #fff;
outline-offset: 1px;
pointer-events: none;
}
.crop-editor-overlay .crop-box .handle {
position: absolute;
}
.crop-editor-overlay .crop-box .handle[data-crop=top] {
cursor: n-resize !important;
}
.crop-editor-overlay .crop-box .handle[data-crop=left] {
cursor: w-resize !important;
}
.crop-editor-overlay .crop-box .handle[data-crop=right] {
cursor: e-resize !important;
}
.crop-editor-overlay .crop-box .handle[data-crop=bottom] {
cursor: s-resize !important;
}
.crop-editor-overlay .crop-box .handle[data-crop=topleft] {
cursor: nw-resize !important;
}
.crop-editor-overlay .crop-box .handle[data-crop=topright] {
cursor: ne-resize !important;
}
.crop-editor-overlay .crop-box .handle[data-crop=bottomleft] {
cursor: sw-resize !important;
}
.crop-editor-overlay .crop-box .handle[data-crop=bottomright] {
cursor: se-resize !important;
}
.crop-editor-overlay .crop-box .handle[data-crop=all] {
cursor: move !important;
}
.crop-editor-overlay .crop-box .handle[data-crop=top] {
width: 100%;
height: 10000px;
bottom: calc(100% - var(--overlap));
}
.crop-editor-overlay .crop-box .handle[data-crop=left] {
width: 10000px;
height: 100%;
right: calc(100% - var(--overlap));
}
.crop-editor-overlay .crop-box .handle[data-crop=right] {
width: 10000px;
height: 100%;
left: calc(100% - var(--overlap));
}
.crop-editor-overlay .crop-box .handle[data-crop=bottom] {
width: 100%;
height: 10000px;
top: calc(100% - var(--overlap));
}
.crop-editor-overlay .crop-box .handle[data-crop=topleft] {
width: 10000px;
height: 10000px;
right: calc(100% - var(--overlap));
bottom: calc(100% - var(--overlap));
}
.crop-editor-overlay .crop-box .handle[data-crop=topright] {
width: 10000px;
height: 10000px;
bottom: calc(100% - var(--overlap));
left: calc(100% - var(--overlap));
}
.crop-editor-overlay .crop-box .handle[data-crop=bottomleft] {
width: 10000px;
height: 10000px;
top: calc(100% - var(--overlap));
right: calc(100% - var(--overlap));
}
.crop-editor-overlay .crop-box .handle[data-crop=bottomright] {
width: 10000px;
height: 10000px;
top: calc(100% - var(--overlap));
left: calc(100% - var(--overlap));
}
.crop-editor-overlay .crop-box .handle[data-crop=all] {
width: 100%;
height: 100%;
left: 0;
}
.inpaint-editor-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.inpaint-editor-overlay.creating-lines {
cursor: crosshair !important;
}
.inpaint-editor-overlay .inpaint-segment {
pointer-events: auto;
}
.inpaint-editor-overlay .inpaint-segment .inpaint-line {
fill: none;
stroke: #f00;
stroke-linecap: round;
stroke-linejoin: round;
stroke-opacity: 0.75;
mix-blend-mode: difference;
}
.inpaint-editor-overlay .inpaint-segment:hover {
pointer-events: all;
}
.inpaint-editor-overlay .inpaint-segment:hover .inpaint-handle {
stroke: #000;
}
.inpaint-editor-overlay .inpaint-segment .inpaint-handle {
opacity: 0;
}
.inpaint-editor-overlay .inpaint-segment.selected .inpaint-handle, .inpaint-editor-overlay .inpaint-segment:hover .inpaint-handle {
opacity: 1;
}
.inpaint-editor-overlay .inpaint-segment .inpaint-handle {
fill: none;
opacity: 0.25;
stroke: #000;
pointer-events: all;
}
.pan-editor-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.pan-editor-overlay .handle {
overflow: visible;
}
.pan-editor-overlay .pan-editor-crop-region {
width: 100%;
height: 100%;
position: relative;
}
.pan-editor-overlay .monitor-preview-box {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
transform-origin: 0 0;
}
.pan-editor-overlay .monitor-preview-box > .box {
box-shadow: 0px 0px 0px 100000px rgba(0, 0, 0, 0.5019607843);
outline: 1px dashed #fff;
width: 100%;
height: 100%;
}
.ranking-data-source .date-row .nav-today {
display: inline-flex;
justify-content: center;
margin: 0 0.25em;
min-width: 5em;
}
.tag-search-with-related-tags {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.tag-search-with-related-tags .search-box {
flex: 1;
}
.slider {
display: flex;
align-items: center;
position: relative;
min-width: 4em;
cursor: pointer;
--fill: 75%;
--track-height: 8px;
--thumb-height: 24px;
--on-color: #07f;
--off-color: #fff;
min-height: var(--thumb-height);
margin: 0 calc(var(--thumb-height) / 2);
}
@media (hover: hover) {
.slider:hover {
--on-color: #0af;
}
}
.slider:active {
--on-color: #0af;
}
.slider .track-left {
width: var(--fill);
height: var(--track-height);
border-radius: var(--border-radius-large);
background-color: var(--on-color);
}
.slider .track-right {
flex: 1;
height: var(--track-height);
border-radius: var(--border-radius-large);
background-color: var(--off-color);
}
.slider .thumb {
height: var(--thumb-height);
aspect-ratio: 1;
background-color: var(--on-color);
position: absolute;
left: var(--fill);
translate: -50% 0;
border-radius: 100%;
}
.menu-dropdown-button {
--box-link-hover-color: transparent;
--box-link-selected-color: transparent;
--box-link-selected-hover-color: transparent;
}
.menu-slider .slider {
flex: 1;
}
/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJzb3VyY2VSb290IjogImZpbGU6Ly8vaG9tZS9ydW5uZXIvd29yay9wcHBpeGl2L3BwcGl4aXYvd2ViIiwKInNvdXJjZXMiOiBbCiJyZXNvdXJjZXMvY3NzL21haW4uc2NzcyIKXSwKIm5hbWVzIjogW10sCiJtYXBwaW5ncyI6ICJBQTJCQTtFQUlFO0VBR0E7RUFJQTtFQUNBOzs7QUFHRjtFQUNFO0VBYUE7RUFDQTtFQVNBO0VBS0E7RUFJQTtFQW1CQTtFQUNBO0VBQ0E7RUFDQTtFQStCQTs7QUFoRkE7RUFLRTs7QUF3QkY7RUFDRTs7QUFzQkE7RUFDRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFJSjtFQUNFOztBQVlGO0VBR0U7OztBQTBDQTtFQUNFO0VBQ0E7O0FBSUo7RUFDRTtFQUNBOztBQUVBO0VBQ0U7O0FBSUo7RUFDRTtFQUNBO0VBTUE7RUFDQTs7QUFMQTtFQUNFOztBQU9KO0VBQ0U7OztBQUlKO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOzs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQU1GO0VBQ0U7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7O0FBR0Y7RUFDRTtFQUNBOzs7QUFLQTtFQUNFOztBQUlBO0VBQ0U7OztBQVNKO0VBQ0U7RUFDQTs7QUFJRjtFQUNFOztBQUtGO0VBQ0U7OztBQUlKO0FBQ0E7RUFHRTtFQUNBO0VBNEJBO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUdBO0VBQ0E7QUFFQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0FBQ0E7RUFFQTtBQUVBO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0FBRUE7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7QUFFQTtBQUFBO0VBRUE7RUFDQTtFQUVBO0FBQ0E7RUFDQTtFQUVBO0VBQ0E7RUFDQTtFQUVBOztBQW5GQTtFQUNFO0VBQ0E7RUFJQTtFQUNBOztBQUdGO0VBQ0U7O0FBSUE7RUFDRTs7QUFLRjtFQUNFOzs7QUFnRU47RUFDRTs7O0FBR0Y7RUFDRTs7O0FBR0Y7QUFBQTtBQUFBO0VBSUU7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtBQUFBO0FBQUE7RUFDRTs7O0FBSUo7QUFBQTtBQUVBO0VBQ0U7OztBQUdGO0VBRUU7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0VBQ0E7RUFDQTs7QUFPSjtFQUNFOztBQUtBO0VBQ0U7O0FBUUo7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTs7QUFJQTtFQUNFOztBQUlKO0FBQUE7RUFFRTs7O0FBS0Y7RUFDRTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTs7QUFFQTtFQUNFO0VBQ0E7RUFDQTs7QUFHRjtFQUNFOzs7QUFPUjtFQUNFOzs7QUFJRjtFQUNFO0VBQ0E7RUFPQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBRUE7O0FBVkE7RUFDRTs7QUFXRjtFQUNFLFlBQ0U7O0FBTUY7RUFRRTs7QUFFQTtFQUNFOztBQU9OO0VBRUU7O0FBRUE7RUFDRTs7QUFHRjtFQUNFOztBQUlKO0VBQ0U7O0FBSUE7RUFDRTs7QUFJRjtFQUNFOztBQVFGO0VBQ0U7RUFDQTtFQUNBOztBQUdGO0VBSUU7O0FBR0E7RUFDRTs7QUFLSjtFQUNFOztBQUlKO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBR0E7O0FBRUE7RUFDRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0VBQ0E7OztBQU1SO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTs7O0FBS047RUFDRTtFQUNBO0VBQ0EsYUFDRTs7O0FBZUo7RUFDRTs7O0FBNEJGO0VBdkJFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBaUJBO0VBQ0E7RUFDQTtFQUlBO0VBTUE7RUFDQTtBQUNBO0VBRUEsWUFDRTtFQUVGOztBQWxDQTtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQWdCRjtFQUNFOztBQVlGO0VBQ0U7OztBQUtGO0VBQ0U7O0FBRUY7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQWdCRjtFQUNFO0VBQ0E7QUFFQTtBQUFBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7QUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTs7QUFJSjtFQUNFO0VBTUE7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFFQTtFQUNBO0VBQ0E7RUFDQTtFQUVBLFlBQ0U7RUFHRjtFQUNBO0VBRUE7RUFDQTs7QUExQkE7RUFDRTtFQUNBO0VBQ0E7O0FBMEJGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBS0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFHQTs7QUFHRjtFQUNFO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUdBO0VBQ0E7RUFDQTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFHRjtFQUNFOzs7QUFLTjtFQUNFO0VBQ0E7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7O0FBR0Y7RUFWRjtJQVlJO0lBQ0E7O0VBRUE7SUFDRTs7RUFFQTtJQUNFO0lBQ0E7SUFDQTs7OztBQU1SO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7OztBQUlKO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUtBO0VBQ0E7RUFDQTs7QUFFQTtBQUFBO0VBRUU7RUFDQTs7QUFJRjtFQUNFO0VBQ0E7RUFDQTs7QUFHRjtFQUNFO0lBQ0U7OztBQUlKO0VBQ0U7O0FBSUY7RUFDRTs7QUFHRjtFQUNFOzs7QUFJSjtFQUNFO0lBQ0U7OztBQUlKO0VBQ0U7RUFHQTs7QUFFQTtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7SUFDRTs7O0FBS047RUFDRTtFQUNBO0VBQ0E7RUFDQTtBQUNBO0VBQ0E7QUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7QUFFQTtBQUFBO0VBRUE7O0FBR0Y7RUFDRTtFQUNBOzs7QUFLTjtFQUdFOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTs7O0FBSUo7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0lBQ0U7SUFDQTs7OztBQUtOO0FBQ0E7QUFBQTtFQUVFOzs7QUFHRjtBQUNBO0VBQ0U7OztBQUdGO0VBQ0U7O0FBRUE7RUFDRTtJQUNFO0lBQ0E7SUFDQTtJQUNBO0lBQ0E7SUFDQTtJQUNBO0lBQ0E7SUFDQTtJQUNBO0lBQ0E7SUFDQTs7RUFHRjtJQUNFOztFQUdGO0lBRUU7O0VBR0Y7SUFDRTtJQUNBOzs7O0FBS047RUFDRTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTs7O0FBR0Y7RUFDRTs7O0FBSUY7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7O0FBSUY7RUFDRTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0U7OztBQUdGO0VBQ0U7OztBQUdGO0FBQ0E7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7OztBQUdGO0VBR0U7RUFHQTs7QUFFQTtFQUNFO0VBQ0E7RUFFQTtFQUNBLFlBQ0U7RUFFRjtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFuaUNGO0VBQ0E7O0FBMmlDRTtFQUNFO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTs7QUFJQTtBQUFBO0FBQUE7RUFHRTs7QUFNSjtFQUNFO0VBQ0E7RUFJQTs7QUFHQTtFQUNFOztBQUdGO0VBQ0U7QUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTs7QUFPTjtFQXZtQ0E7RUFDQTtFQStuQ0U7RUFDQTtFQUNBO0VBQ0E7RUFFQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBR0E7RUFFQTtFQUNBOztBQXRDQTtFQUNFO0VBQ0E7O0FBT0Y7RUFDRTtFQUNBOztBQU1GO0VBQ0U7RUFDQTs7QUFxQkY7RUFLRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUNBOztBQUlKO0VBRUU7RUFDQTtFQUdBO0VBQ0E7O0FBR0Y7RUFHRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFFRTs7QUFHQTtFQUNFOztBQUlKO0VBQ0U7O0FBSUY7RUFTRTs7QUFKQTtFQUNFOztBQU9GO0VBRUU7O0FBS047RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFOztBQUdGO0VBQ0U7RUFDQTs7QUFFQTtFQUNFO0VBQ0E7O0FBSUo7RUFDRTtFQUNBOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0FBQ0E7O0FBSUo7RUFDRTs7QUFJSjtFQUNFOzs7QUFJSjtFQUdFO0VBQ0E7RUFDQTs7O0FBT0E7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUdGO0VBQ0U7SUFDRTs7O0FBSUo7RUF6dkJBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBa3ZCRTs7QUFodkJGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBMnVCQTtFQUNFOztBQUtKO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFJQTtFQUdBOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFJRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFHQTtBQVVBO0FBQUE7O0FBUkE7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUtGO0VBQ0U7RUFDQTs7QUFLSjtFQUNFO0VBQ0E7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0FBWUE7O0FBVkE7RUFDRTtBQUVBO0FBQUE7RUFFQTtFQUNBO0VBQ0E7O0FBSUY7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0VBQ0E7O0FBTUo7RUFsQ0Y7SUFzQ0k7SUFjQTs7RUFaQTtJQUNFOztFQUdGO0lBQ0U7SUFDQTs7RUFRRjtJQUNFOzs7QUFJSjtFQTZCRTs7QUE1QkE7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7RUFFQTs7QUFFQTtFQUNFO0VBQ0E7RUFDQTs7QUFJRjtFQUNFOztBQVVKO0VBQ0U7RUFDQTs7QUFHRjtFQUNFOztBQUlKO0VBQ0U7O0FBR0Y7RUFFRTtJQUNFOzs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBS0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTs7QUFXRjtBQUFBO0VBRUU7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUdBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBOztBQUVBO0FBQ0U7QUFBQTtFQUVBOztBQUlKO0VBQ0U7O0FBSUo7RUFDRTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBSUE7RUFDRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7O0FBR0E7RUFDRTs7QUFLRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTs7QUFLTjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUtOO0VBRUk7QUFBQTtBQUFBO0lBR0U7OztBQUtOO0VBQ0U7O0FBR0Y7RUFFRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUlBO0VBQ0U7RUFJQTtFQUVBO0VBQ0E7O0FBRUE7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7O0FBSUY7RUFDRTs7QUFHRjtFQUNFOzs7QUFXVjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUlBO0VBQ0U7RUFDQTs7O0FBS0o7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTs7QUFPRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTs7QUFPQTtFQUNFOzs7QUFTTjtFQUdFO0VBRUE7RUFDQTs7QUFJQTtFQUNFO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFFQTtFQUNBOzs7QUFJSjtFQUNFOzs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUlBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7O0FBR0Y7RUFFRTtFQUNBOztBQUlGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFsN0RGO0VBQ0U7SUFzN0RFOztFQUVBO0lBQ0U7OztBQXA3RE47RUFpN0RJOztBQUVBO0VBQ0U7O0FBS047RUFDRTtFQUNBO0VBR0E7O0FBR0Y7QUFDRTtBQUFBO0VBRUE7RUFDQTtFQUNBOztBQUlGO0VBQ0U7RUFLQTtFQUNBOztBQUlBO0VBQ0U7RUFLQTs7QUFHRjtFQUVFOzs7QUFLTjtFQUNFOzs7QUFJRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7OztBQUlKO0VBQ0U7RUFDQTtFQUVBO0VBTUE7RUFDQTtFQUNBOztBQU5BO0VBQ0U7O0FBT0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUdBOztBQVFGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7OztBQUtOO0FBRUU7RUFDRTtFQUNBO0FBQ0E7O0FBR0Y7RUFDRTtFQUNBO0FBQ0E7OztBQUtGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0EsYUFDRTs7O0FBT047RUFDRTtFQUNBOzs7QUFHRjtFQUNFO0VBSUE7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFJQTtFQUNFO0VBQ0E7O0FBSUY7RUFDRTtFQUNBOzs7QUFLSjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBSUU7QUFBQTtFQUVFOztBQUlKO0FBQUE7RUFFRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtBQUFBO0VBQ0U7RUFDQTs7QUFHRjtFQUNFO0FBQUE7SUFDRTs7O0FBS047RUFDRTtFQUNBOztBQUdGO0VBQ0U7RUFFQTtFQUNBO0VBQ0E7RUFHQTs7QUFHQTtFQUNFOztBQUlGO0VBQ0U7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0FBQUE7RUFFRTs7QUFNSjtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBOztBQUlKO0VBQ0U7O0FBT0Y7RUFDRTtJQUNFOztFQUdGO0lBQ0U7OztBQU1OO0VBQ0U7OztBQUtOO0VBQ0U7OztBQUdGO0FBQ0E7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQSxZQUNFO0VBS0Y7RUFDQTtBQWNBOztBQVhBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTs7QUFJRjtFQUNFOztBQUlGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTs7QUFLTjtFQUNFO0VBQ0E7RUFDQTtBQWFBO0FBS0E7QUFBQTs7QUFoQkE7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUVBO0VBQ0E7RUFDQTs7QUFJRjtFQUNFOztBQUtGO0VBQ0U7O0FBRUY7RUFDRTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBLGFBQ0U7RUFJRjtFQUNBO0VBQ0E7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtBQU9BO0FBa0JBO0FBQUE7QUFVQTs7QUFqQ0E7RUFDRTtFQUNBOztBQUlGO0VBQ0U7RUFDQTs7QUFHRjtFQUNFO0lBQ0U7SUFDQTs7O0FBSUo7RUFDRTtFQUNBOztBQUtGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFJRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBS0o7RUFDRTs7QUFHRjtFQUNFOztBQUlKO0VBRUU7RUFDQTtFQUNBO0VBSUE7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUdGO0VBR0U7OztBQU1BO0VBQ0U7O0FBR0Y7RUFDRTs7O0FBS047QUFBQTtBQUVFO0FBQUE7QUFVQTs7QUFSQTtBQUFBO0VBQ0U7O0FBR0Y7QUFBQTtFQUNFOztBQUlGO0FBQUE7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBUUE7O0FBTkE7QUFBQTtFQUNFO0VBQ0E7RUFDQTs7QUFLRjtBQUFBO0VBQ0U7O0FBR0Y7RUFDRTtBQUFBO0lBQ0U7O0VBRUE7QUFBQTtJQUNFOzs7O0FBT1Y7RUFDRTs7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBR0E7RUFFQTtFQVNBO0VBQ0E7O0FBUkE7RUFDRTs7QUFTRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUVBOztBQUVBO0VBQ0U7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUVFOztBQUdGO0VBQ0U7RUFDQTs7QUFJSjtFQUNFO0VBQ0E7RUFJQTtFQUNBO0VBQ0E7RUFDQTtFQUdBO0VBRUE7RUFDQTtFQUNBOztBQUlKO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBR0E7RUFVQTtBQWNBOztBQXRCQTtFQUNFOztBQUdGO0VBQ0U7O0FBS0Y7RUFDRTtFQUNBOztBQUdGO0VBQ0U7RUFHQTs7QUFJRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTs7O0FBTU47RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0VBQ0E7O0FBRUE7RUFDRTtFQUNBOztBQUlKO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBRUU7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7OztBQU1SO0FBQUE7RUFFRTs7O0FBR0Y7QUFBQTtBQUVBO0VBQ0U7OztBQUtBO0VBQ0U7SUFDRTs7O0FBSUo7RUFDRTtJQUNFO0lBQ0E7Ozs7QUFLTjtBQUNFO0FBQUE7QUFBQTtBQUFBOztBQUlBO0VBQ0U7O0FBR0Y7RUFDRTs7QUFHRjtFQUNFO0lBQ0U7Ozs7QUFNSjtFQUNFO0VBQ0E7O0FBR0Y7RUFDRTtJQUNFOzs7O0FBS047RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTtJQUNFO0lBQ0E7SUFDQTtJQUNBO0lBQ0E7SUFDQTs7O0FBS0Y7RUFDRTtFQUNBOztBQUdGO0VBQ0U7OztBQU1OO0VBQ0U7OztBQUdGO0VBQ0U7OztBQTZRRjtFQTNQRTtFQUNBO0VBQ0E7RUFDQTtFQVNBO0VBSUE7RUFDQTtFQU1BO0VBa0JBO0VBQ0E7RUFTQTtFQUNBO0VBT0E7RUFDQTtFQXNEQTs7QUEzR0E7RUFDRTs7QUFXRjtFQUNFOztBQVNGO0VBRUU7RUFDQTs7QUFHRjtFQUVFO0VBQ0E7O0FBUUY7RUFDRTs7QUFPRjtFQUNFO0VBQ0E7O0FBT0Y7RUFFRTtFQUNBO0VBQ0E7RUFDQTtFQUVBO0VBQ0E7RUFDQTtFQUNBOztBQUdBO0VBQ0U7O0FBSUo7RUFDRTtFQUNBO0VBQ0E7RUFNQTtFQUNBO0VBQ0E7RUFDQTtFQUVBO0VBQ0E7RUFDQTtFQUNBOztBQUdBO0VBQ0U7RUFDQTs7QUFjSjtFQUNFO0VBSUE7O0FBSUE7RUFDRTtFQUNBO0VBQ0E7RUFRQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBU0o7RUFDRTtFQUNBO0VBSUE7RUFFQTs7QUFHRjtFQUVFO0VBQ0E7O0FBSUY7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFJQTtFQUlFOztBQVFGO0VBQ0U7O0FBSUo7RUFDRTtFQUVBO0VBQ0E7RUFDQTs7QUFJQTtFQUNFOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtJQUNFOzs7QUFJSjtFQUNFOztBQUtOO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBS0Y7RUFFRTtFQUNBO0VBQ0E7OztBQVNOO0VBL1BFO0VBQ0E7RUFDQTtFQUNBO0VBU0E7RUFJQTtFQUNBO0VBTUE7RUFrQkE7RUFDQTtFQVNBO0VBQ0E7RUFPQTtFQUNBO0VBc0RBOztBQTNHQTtFQUNFOztBQVdGO0VBQ0U7O0FBU0Y7RUFFRTtFQUNBOztBQUdGO0VBRUU7RUFDQTs7QUFRRjtFQUNFOztBQU9GO0VBQ0U7RUFDQTs7QUFPRjtFQUVFO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQU1BO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTtFQUNBOztBQWNKO0VBQ0U7RUFJQTs7QUFJQTtFQUNFO0VBQ0E7RUFDQTtFQVFBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFTSjtFQUNFO0VBQ0E7RUFJQTtFQUVBOztBQUdGO0VBRUU7RUFDQTs7QUFJRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUlBO0VBSUU7O0FBUUY7RUFDRTs7QUFJSjtFQUNFO0VBRUE7RUFDQTtFQUNBOztBQUlBO0VBQ0U7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0lBQ0U7OztBQUlKO0VBQ0U7O0FBS047RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFLRjtFQUVFO0VBQ0E7RUFDQTs7O0FBY047RUFwUUU7RUFDQTtFQUNBO0VBQ0E7RUFTQTtFQUlBO0VBQ0E7RUFNQTtFQWtCQTtFQUNBO0VBU0E7RUFDQTtFQU9BO0VBQ0E7RUFzREE7RUFvSkE7O0FBL1BBO0VBQ0U7O0FBV0Y7RUFDRTs7QUFTRjtFQUVFO0VBQ0E7O0FBR0Y7RUFFRTtFQUNBOztBQVFGO0VBQ0U7O0FBT0Y7RUFDRTtFQUNBOztBQU9GO0VBRUU7RUFDQTtFQUNBO0VBQ0E7RUFFQTtFQUNBO0VBQ0E7RUFDQTs7QUFHQTtFQUNFOztBQUlKO0VBQ0U7RUFDQTtFQUNBO0VBTUE7RUFDQTtFQUNBO0VBQ0E7RUFFQTtFQUNBO0VBQ0E7RUFDQTs7QUFHQTtFQUNFO0VBQ0E7O0FBY0o7RUFDRTtFQUlBOztBQUlBO0VBQ0U7RUFDQTtFQUNBO0VBUUE7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQVNKO0VBQ0U7RUFDQTtFQUlBO0VBRUE7O0FBR0Y7RUFFRTtFQUNBOztBQUlGO0VBQ0U7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBSUE7RUFJRTs7QUFRRjtFQUNFOztBQUlKO0VBQ0U7RUFFQTtFQUNBO0VBQ0E7O0FBSUE7RUFDRTs7QUFFQTtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7SUFDRTs7O0FBSUo7RUFDRTs7QUFLTjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUtGO0VBRUU7RUFDQTtFQUNBOztBQWtCSjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7OztBQUtOO0VBQ0U7RUFDQTs7QUFFQTtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBO0FBQ0E7O0FBR0Y7RUFDRTtFQUNBOztBQUdGO0VBQ0U7OztBQUlKO0VBblRFO0VBQ0E7RUFDQTtFQUNBO0VBU0E7RUFJQTtFQUNBO0VBTUE7RUFrQkE7RUFDQTtFQVNBO0VBQ0E7RUFPQTtFQUNBO0VBc0RBO0VBbU1BOztBQTlTQTtFQUNFOztBQVdGO0VBQ0U7O0FBU0Y7RUFFRTtFQUNBOztBQUdGO0VBRUU7RUFDQTs7QUFRRjtFQUNFOztBQU9GO0VBQ0U7RUFDQTs7QUFPRjtFQUVFO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQU1BO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTtFQUNBOztBQWNKO0VBQ0U7RUFJQTs7QUFJQTtFQUNFO0VBQ0E7RUFDQTtFQVFBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFTSjtFQUNFO0VBQ0E7RUFJQTtFQUVBOztBQUdGO0VBRUU7RUFDQTs7QUFJRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUlBO0VBSUU7O0FBUUY7RUFDRTs7QUFJSjtFQUNFO0VBRUE7RUFDQTtFQUNBOztBQUlBO0VBQ0U7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0lBQ0U7OztBQUlKO0VBQ0U7O0FBS047RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFLRjtFQUVFO0VBQ0E7RUFDQTs7QUFpRUo7RUFFRTs7QUFFQTtFQUdFO0VBQ0E7O0FBSUo7RUFDRTtFQUlBOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFJQTtFQUNBO0VBQ0E7RUFDQTs7O0FBSUo7RUE3VkU7RUFDQTtFQUNBO0VBQ0E7RUFTQTtFQUlBO0VBQ0E7RUFNQTtFQWtCQTtFQUNBO0VBU0E7RUFDQTtFQU9BO0VBQ0E7RUFzREE7O0FBM0dBO0VBQ0U7O0FBV0Y7RUFDRTs7QUFTRjtFQUVFO0VBQ0E7O0FBR0Y7RUFFRTtFQUNBOztBQVFGO0VBQ0U7O0FBT0Y7RUFDRTtFQUNBOztBQU9GO0VBRUU7RUFDQTtFQUNBO0VBQ0E7RUFFQTtFQUNBO0VBQ0E7RUFDQTs7QUFHQTtFQUNFOztBQUlKO0VBQ0U7RUFDQTtFQUNBO0VBTUE7RUFDQTtFQUNBO0VBQ0E7RUFFQTtFQUNBO0VBQ0E7RUFDQTs7QUFHQTtFQUNFO0VBQ0E7O0FBY0o7RUFDRTtFQUlBOztBQUlBO0VBQ0U7RUFDQTtFQUNBO0VBUUE7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQVNKO0VBQ0U7RUFDQTtFQUlBO0VBRUE7O0FBR0Y7RUFFRTtFQUNBOztBQUlGO0VBQ0U7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBSUE7RUFJRTs7QUFRRjtFQUNFOztBQUlKO0VBQ0U7RUFFQTtFQUNBO0VBQ0E7O0FBSUE7RUFDRTs7QUFFQTtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7SUFDRTs7O0FBSUo7RUFDRTs7QUFLTjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUtGO0VBRUU7RUFDQTtFQUNBOztBQTBHSjtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUdGO0VBQ0U7SUFDRTs7OztBQU1SO0VBaFlFO0VBQ0E7RUFDQTtFQUNBO0VBU0E7RUFJQTtFQUNBO0VBTUE7RUFrQkE7RUFDQTtFQVNBO0VBQ0E7RUFPQTtFQUNBO0VBc0RBOztBQTNHQTtFQUNFOztBQVdGO0VBQ0U7O0FBU0Y7RUFFRTtFQUNBOztBQUdGO0VBRUU7RUFDQTs7QUFRRjtFQUNFOztBQU9GO0VBQ0U7RUFDQTs7QUFPRjtFQUVFO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQU1BO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTtFQUNBOztBQWNKO0VBQ0U7RUFJQTs7QUFJQTtFQUNFO0VBQ0E7RUFDQTtFQVFBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFTSjtFQUNFO0VBQ0E7RUFJQTtFQUVBOztBQUdGO0VBRUU7RUFDQTs7QUFJRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUlBO0VBSUU7O0FBUUY7RUFDRTs7QUFJSjtFQUNFO0VBRUE7RUFDQTtFQUNBOztBQUlBO0VBQ0U7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0lBQ0U7OztBQUlKO0VBQ0U7O0FBS047RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFLRjtFQUVFO0VBQ0E7RUFDQTs7QUE2SUo7RUFDRTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBOzs7QUFPTjtFQUlFO0VBQ0E7RUFTQTtFQUtBOztBQUlBO0VBQ0U7RUFDQTtFQUNBO0VBSUE7O0FBSUE7RUFscUdGO0VBQ0E7O0FBc3FHSTtFQUNFO0VBQ0E7RUFDQTs7QUFLTjtFQUNFO0VBQ0E7RUFHQTtFQUNBO0VBR0E7O0FBS0Y7RUFHRTtFQUNBO0VBQ0E7RUFFQTtFQUdBO0VBTUE7O0FBR0E7RUFDRTtFQUdBO0VBRUE7RUFJQTs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFLQTs7QUFFQTtFQUNFO0VBQ0E7RUFFQTs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7QUFpQkE7O0FBZkE7RUFDRTtFQUNBOztBQUdGO0VBQ0U7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7O0FBSUY7RUFDRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFJSjtFQUNFO0lBQ0U7O0VBR0Y7SUFDRTs7RUFHRjtJQUNFOzs7QUFJSjtFQUNFO0lBQ0U7O0VBR0Y7SUFDRTs7RUFHRjtJQUNFOzs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBR0E7RUFNQTs7QUFKQTtFQUNFOztBQUtGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7OztBQU1KO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTtFQUNBO0VBSUE7O0FBRUE7RUFDRTtFQUVBOztBQUVBO0VBQ0U7SUFDRTs7RUFHRjtJQUNFOztFQUdGO0lBQ0U7O0VBR0Y7SUFDRTs7OztBQU9WO0VBQ0U7RUFDQTtFQUNBO0FBRUE7QUFBQTtBQUFBOztBQUdBO0VBQ0U7RUFDQTs7O0FBSUo7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFNQTs7QUFFQTtFQUNFOztBQUlGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBRUU7O0FBR0Y7RUFFRTtFQUNBOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFFQTtFQUNBO0VBQ0E7O0FBRUE7RUFFRTtFQUNBO0VBQ0E7RUFDQTs7QUFJRjtFQUNFOztBQUdGO0VBQ0U7O0FBSUY7RUFDRTs7QUFHRjtFQUVFOztBQUdGO0VBQ0U7RUFDQTs7QUFHRjtFQUNFOzs7QUFPVjtFQUNFO0VBQ0E7RUFRQTs7QUFKQTtFQUNFOzs7QUFNSjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0EsWUFDRTtFQUVGO0VBQ0E7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7OztBQUlKO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFHRTs7O0FBSUo7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0VBQ0E7RUFDQTs7O0FBNkdKO0VBNThCRTtFQUNBO0VBQ0E7RUFDQTtFQVNBO0VBSUE7RUFDQTtFQU1BO0VBa0JBO0VBQ0E7RUFTQTtFQUNBO0VBT0E7RUFDQTtFQXNEQTtFQXN2QkE7O0FBajJCQTtFQUNFOztBQVdGO0VBQ0U7O0FBU0Y7RUFFRTtFQUNBOztBQUdGO0VBRUU7RUFDQTs7QUFRRjtFQUNFOztBQU9GO0VBQ0U7RUFDQTs7QUFPRjtFQUVFO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQU1BO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTtFQUNBOztBQWNKO0VBQ0U7RUFJQTs7QUFJQTtFQUNFO0VBQ0E7RUFDQTtFQVFBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFTSjtFQUNFO0VBQ0E7RUFJQTtFQUVBOztBQUdGO0VBRUU7RUFDQTs7QUFJRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUlBO0VBSUU7O0FBUUY7RUFDRTs7QUFJSjtFQUNFO0VBRUE7RUFDQTtFQUNBOztBQUlBO0VBQ0U7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0lBQ0U7OztBQUlKO0VBQ0U7O0FBS047RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFLRjtFQUVFO0VBQ0E7RUFDQTs7QUF1bkJKO0VBQ0U7RUFDQTs7QUFJQTtFQUNFOztBQUtKO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBOztBQUVBO0VBQ0U7O0FBR0Y7RUFDRTs7QUFHRjtFQUNFO0lBQ0U7OztBQUtOO0VBQ0U7O0FBTUo7RUFDRTtFQUlBOztBQUdGO0VBQ0U7RUFDQTtFQUNBOztBQUVBO0VBQ0U7O0FBR0Y7RUFFRTtFQUNBO0VBQ0E7RUFDQTs7QUFHQTtFQUNFOztBQUlGO0VBQ0U7RUFDQTs7QUFNQTtFQUNFOztBQUlKO0VBQ0U7RUFDQTs7QUFZSjtFQUNFOztBQU1GO0VBQ0U7O0FBSUo7RUFFRTtFQUNBO0VBR0E7OztBQUlKO0VBeCtCRTtFQUNBO0VBQ0E7RUFDQTtFQVNBO0VBSUE7RUFDQTtFQU1BO0VBa0JBO0VBQ0E7RUFTQTtFQUNBO0VBT0E7RUFDQTtFQXNEQTtFQXN2QkE7O0FBajJCQTtFQUNFOztBQVdGO0VBQ0U7O0FBU0Y7RUFFRTtFQUNBOztBQUdGO0VBRUU7RUFDQTs7QUFRRjtFQUNFOztBQU9GO0VBQ0U7RUFDQTs7QUFPRjtFQUVFO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTs7QUFJSjtFQUNFO0VBQ0E7RUFDQTtFQU1BO0VBQ0E7RUFDQTtFQUNBO0VBRUE7RUFDQTtFQUNBO0VBQ0E7O0FBR0E7RUFDRTtFQUNBOztBQWNKO0VBQ0U7RUFJQTs7QUFJQTtFQUNFO0VBQ0E7RUFDQTtFQVFBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFTSjtFQUNFO0VBQ0E7RUFJQTtFQUVBOztBQUdGO0VBRUU7RUFDQTs7QUFJRjtFQUNFOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUlBO0VBSUU7O0FBUUY7RUFDRTs7QUFJSjtFQUNFO0VBRUE7RUFDQTtFQUNBOztBQUlBO0VBQ0U7O0FBRUE7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0lBQ0U7OztBQUlKO0VBQ0U7O0FBS047RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFLRjtFQUVFO0VBQ0E7RUFDQTs7QUF1bkJKO0VBQ0U7RUFDQTs7QUFJQTtFQUNFOztBQUtKO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBOztBQUVBO0VBQ0U7O0FBR0Y7RUFDRTs7QUFHRjtFQUNFO0lBQ0U7OztBQUtOO0VBQ0U7O0FBTUo7RUFDRTtFQUlBOztBQUdGO0VBQ0U7RUFDQTtFQUNBOztBQUVBO0VBQ0U7O0FBR0Y7RUFFRTtFQUNBO0VBQ0E7RUFDQTs7QUFHQTtFQUNFOztBQUlGO0VBQ0U7RUFDQTs7QUFNQTtFQUNFOztBQUlKO0VBQ0U7RUFDQTs7O0FBc0NSO0VBNStCRTtFQUNBO0VBQ0E7RUFDQTtFQVNBO0VBSUE7RUFDQTtFQU1BO0VBa0JBO0VBQ0E7RUFTQTtFQUNBO0VBT0E7RUFDQTtFQXNEQTs7QUEzR0E7RUFDRTs7QUFXRjtFQUNFOztBQVNGO0VBRUU7RUFDQTs7QUFHRjtFQUVFO0VBQ0E7O0FBUUY7RUFDRTs7QUFPRjtFQUNFO0VBQ0E7O0FBT0Y7RUFFRTtFQUNBO0VBQ0E7RUFDQTtFQUVBO0VBQ0E7RUFDQTtFQUNBOztBQUdBO0VBQ0U7O0FBSUo7RUFDRTtFQUNBO0VBQ0E7RUFNQTtFQUNBO0VBQ0E7RUFDQTtFQUVBO0VBQ0E7RUFDQTtFQUNBOztBQUdBO0VBQ0U7RUFDQTs7QUFjSjtFQUNFO0VBSUE7O0FBSUE7RUFDRTtFQUNBO0VBQ0E7RUFRQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBU0o7RUFDRTtFQUNBO0VBSUE7RUFFQTs7QUFHRjtFQUVFO0VBQ0E7O0FBSUY7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFJQTtFQUlFOztBQVFGO0VBQ0U7O0FBSUo7RUFDRTtFQUVBO0VBQ0E7RUFDQTs7QUFJQTtFQUNFOztBQUVBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtJQUNFOzs7QUFJSjtFQUNFOztBQUtOO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBS0Y7RUFFRTtFQUNBO0VBQ0E7O0FBeXZCSjtFQUNFO0VBQ0E7RUFDQTs7QUFFQTtFQUNFO0VBRUE7RUFHQTs7O0FBS047RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBOzs7QUFLQTtBQUFBO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBSUE7QUFBQTtFQUNFO0VBQ0E7O0FBSUo7QUFBQTtFQUdFOztBQUVBO0FBQUE7RUFDRTtFQUNBOztBQUlKO0FBQUE7RUFDRTtFQUNBO0VBQ0E7O0FBRUE7QUFBQTtFQUNFO0VBQ0E7RUFDQTs7QUFFQTtBQUFBO0VBQ0U7O0FBS047QUFBQTtFQUNFO0VBQ0E7OztBQUlKO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBRUE7RUFDRTtFQUNBOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFHRjtFQWNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFqQkE7RUFDRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7O0FBVUY7RUFDRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFJQTtFQUNFOztBQUtOO0VBQ0U7SUFDRTs7RUFHRjtJQUNFOzs7O0FBS047RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBOztBQUVBO0VBQ0U7RUFHQTs7QUFFQTtFQUVFOztBQUdGO0VBQ0U7RUFDQTs7QUFHRjtFQUVFO0VBQ0E7RUFJQTs7QUFHRjtFQUNFOztBQUVBO0VBQ0U7O0FBR0Y7RUFDRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFHRjtFQUNFOztBQUdGO0VBQ0U7O0FBR0Y7RUFDRTs7QUFHRjtFQUNFOztBQUlGO0VBQ0U7RUFDQTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7OztBQU1SO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUNFOztBQUdGO0VBRUU7O0FBQ0E7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTs7QUFFQTtFQUNFOztBQUlKO0VBQ0U7O0FBR0Y7RUFFRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBOzs7QUFLTjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7O0FBS0E7RUFDRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7QUFFQTtFQUVFO0VBQ0E7RUFDQTtFQUNBOzs7QUFTRjtFQUNFO0VBQ0E7RUFDQTtFQUNBOzs7QUFLTjtFQUNFO0VBQ0E7RUFDQTs7QUFFQTtFQUNFOzs7QUFJSjtFQUNFO0VBQ0E7RUFHQTtFQUNBO0VBQ0E7RUFHQTtFQUdBO0VBQ0E7RUFFQTtFQUNBO0VBSUE7RUFHQTs7QUFydklBO0VBQ0U7SUF1dklBOzs7QUFsdklGO0VBa3ZJRTs7QUFHRjtFQUNFO0VBQ0E7RUFDQTtFQUNBOztBQUdGO0VBQ0U7RUFDQTtFQUNBO0VBQ0E7O0FBR0Y7RUFDRTtFQUNBO0VBQ0E7RUFDQTtFQUNBO0VBQ0E7RUFDQTs7O0FBSUo7RUFFRTtFQUNBO0VBQ0E7OztBQUdGO0VBQ0UiLAoiZmlsZSI6ICJ3ZWIvcmVzb3VyY2VzL2Nzcy9tYWluLnNjc3MiCn0= */\`;
/\x2f output/intermediate/app-resources.js
function getResources() {
return {
"resources/noise.png": noise_default,
"resources/apple-touch-icon.png": apple_touch_icon_default,
"resources/favorited-icon.png": favorited_icon_default,
"resources/heart-icon-private.svg": heart_icon_private_default,
"resources/exit-icon.svg": exit_icon_default,
"resources/activate-icon.png": activate_icon_default,
"resources/vview-icon.png": vview_icon_default,
"resources/ppixiv.woff": ppixiv_default,
"resources/heart-icon.svg": heart_icon_default,
"resources/pan-editor-marker.svg": pan_editor_marker_default,
"resources/multi-monitor.svg": multi_monitor_default,
"resources/regular-pixiv-icon.png": regular_pixiv_icon_default,
"resources/download-icon.svg": download_icon_default,
"resources/download-manga-icon.svg": download_manga_icon_default,
"resources/last-viewed-image-marker.svg": last_viewed_image_marker_default,
"resources/last-page.svg": last_page_default,
"resources/icon-twitter.svg": icon_twitter_default,
"resources/zoom-actual.svg": zoom_actual_default,
"resources/play-button.svg": play_button_default,
"resources/followed.svg": followed_default,
"resources/manifest.json": manifest_default,
"resources/picture-in-picture.svg": picture_in_picture_default,
"resources/eye-icon.svg": eye_icon_default,
"resources/zoom-minus.svg": zoom_minus_default,
"resources/page-icon.png": page_icon_default,
"resources/followed-users-eye.svg": followed_users_eye_default,
"resources/zoom-full.svg": zoom_full_default,
"resources/folder.svg": folder_default,
"resources/fullscreen.svg": fullscreen_default,
"resources/like-button.svg": like_button_default,
"resources/ai.png": ai_default,
"resources/close-button.svg": close_button_default,
"resources/favicon.png": favicon_default,
"resources/page-icon-hover.png": page_icon_hover_default,
"resources/index.html": resources_default,
"resources/zoom-plus.svg": zoom_plus_default,
"resources/auth.html": auth_default,
"resources/css/main.scss": main_default
};
}
/\x2f web/vview/app.js
var App = class {
constructor() {
ppixiv.app = this;
this.setup();
}
async setup() {
console.log(\`\${ppixiv.native ? "vview" : "ppixiv"} controller setup\`);
if (!ppixiv.native)
this._temporarilyHideDocument();
await helpers.other.waitForContentLoaded();
await init(this);
installPolyfills();
ppixiv.phistory = new VirtualHistory({ permanent: ppixiv.mobile });
ppixiv.settings = new Settings();
ppixiv.mediaCache = new MediaCache();
ppixiv.userCache = new UserCache();
ppixiv.extraImageData = new ExtraImageData();
ppixiv.extraCache = new ExtraCache();
ppixiv.sendImage = new SendImage();
ppixiv.tagTranslations = new TagTranslations();
ppixiv.guessImageUrl = new GuessImageURL();
ppixiv.muting = new Muting();
ppixiv.imageTranslations = new ImageTranslations();
PointerListener.installGlobalHandler();
new PointerEventMovement();
let refreshFocus = () => {
helpers.html.setClass(document.body, "focused", document.hasFocus());
};
window.addEventListener("focus", refreshFocus);
window.addEventListener("blur", refreshFocus);
refreshFocus();
window.history.scrollRestoration = "manual";
if (ppixiv.mobile) {
window.addEventListener("contextmenu", (e) => {
e.preventDefault();
});
window.addEventListener("dragstart", (e) => {
e.preventDefault();
});
helpers.forceTargetBlank();
}
if (ppixiv.site == null) {
if (ppixiv.native)
ppixiv.site = new SiteNative();
else
ppixiv.site = new SitePixiv();
}
if (!await ppixiv.site.init())
return;
await ppixiv.site.setInitialUrl();
window.addEventListener("click", this._windowClickCapture);
window.addEventListener("popstate", this._windowRedirectPopstate, true);
window.addEventListener("pp:popstate", this._windowPopstate);
window.addEventListener("keyup", this._redirectEventToScreen, true);
window.addEventListener("keydown", this._redirectEventToScreen, true);
window.addEventListener("keypress", this._redirectEventToScreen, true);
window.addEventListener("keydown", this._windowKeydown);
let disableDarkReader = document.realCreateElement("meta");
disableDarkReader.name = "darkreader-lock";
document.head.appendChild(disableDarkReader);
this.loadResourceBlobs();
helpers.html.addStyle(
"ppixiv-font",
\`
@font-face {
font-family: 'ppixiv';
src: url(\${ppixiv.resources["resources/ppixiv.woff"]}) format('woff');
font-weight: normal;
font-style: normal;
font-display: block;
}
\`
);
document.head.appendChild(helpers.html.createStyle(main_default, { id: "main" }));
let initialStylesheet = document.querySelector("#initial-style");
if (initialStylesheet)
initialStylesheet.remove();
if (ppixiv.ios && document.querySelector("meta[name='viewport']") == null) {
let meta = document.createElement("meta");
meta.setAttribute("name", "viewport");
meta.setAttribute(
"content",
"viewport-fit=cover, initial-scale=1, user-scalable=no"
);
document.head.appendChild(meta);
}
{
let meta = document.createElement("meta");
meta.setAttribute("name", "theme-color");
meta.setAttribute("content", "#000");
document.head.appendChild(meta);
}
this._undoTemporarilyHideDocument();
this._setDeviceProperties();
ppixiv.settings.addEventListener("display_mode", this._setDeviceProperties);
window.addEventListener("orientationchange", this._setDeviceProperties);
new ResizeObserver(this._setDeviceProperties).observe(
document.documentElement
);
ppixiv.message = new MessageWidget({ container: document.body });
load();
if (document.querySelector("title") == null)
document.head.appendChild(document.createElement("title"));
let documentIcon = document.head.appendChild(
document.createElement("link")
);
documentIcon.setAttribute("rel", "icon");
if (window.opener?.slideshowStagingDialog == window) {
new SlideshowStagingDialog();
return;
}
this.addClicksToSearchHistory(document.body);
if (!ppixiv.mobile)
this._contextMenu = new ContextMenu({ container: document.body });
LinkThisTabPopup.setup();
SendHerePopup.setup();
WhatsNew.handleLastViewedVersion();
this._screenSearch = new ScreenSearch({
container: document.body,
visible: false
});
this._screenIllust = new ScreenIllust({
container: document.body,
visible: false
});
this._currentScreen = null;
this.setCurrentDataSource({ cause: "initialization" });
}
_windowRedirectPopstate = (e) => {
e.stopImmediatePropagation();
let e2 = new Event("pp:popstate");
e.target.dispatchEvent(e2);
};
_windowPopstate = (e) => {
this.setCurrentDataSource({
cause: e.navigationCause || "history",
scrollToTop: e.scrollToTop
});
};
_setDeviceProperties = () => {
let insets = helpers.html.getSafeAreaInsets();
helpers.html.setClass(document.documentElement, "mobile", ppixiv.mobile);
let firefox = navigator.userAgent.indexOf("Gecko/") != -1 || navigator.userAgent.indexOf("Firefox/") != -1;
helpers.html.setClass(document.documentElement, "firefox", firefox);
helpers.html.setClass(
document.documentElement,
"macos",
navigator.userAgent.indexOf("Macintosh") != -1
);
helpers.html.setClass(document.documentElement, "ios", ppixiv.ios);
helpers.html.setClass(document.documentElement, "android", ppixiv.android);
helpers.html.setClass(
document.documentElement,
"phone",
helpers.other.isPhone()
);
document.documentElement.dataset.orientation = window.orientation ?? "0";
helpers.html.setDataSet(
document.documentElement.dataset,
"hasBottomInset",
insets.bottom > 0
);
let testOverlayScrollbars = document.realCreateElement("div");
testOverlayScrollbars.classList.add("overlay-scrollbar-tester");
testOverlayScrollbars.style.position = "absolute";
testOverlayScrollbars.style.visibility = "hidden";
testOverlayScrollbars.style.scrollbarGutter = "stable both-edges";
testOverlayScrollbars.style.overflowY = "auto";
testOverlayScrollbars.style.width = "100px";
testOverlayScrollbars.style.height = "100px";
document.body.appendChild(testOverlayScrollbars);
let hasOverlayScrollbars = testOverlayScrollbars.offsetWidth == testOverlayScrollbars.scrollWidth;
helpers.html.setClass(
document.documentElement,
"has-overlay-scrollbars",
hasOverlayScrollbars
);
testOverlayScrollbars.remove();
let displayMode = ppixiv.settings.get("display_mode", "auto");
if (["auto", "normal", "notch", "safe"].indexOf(displayMode) == -1)
displayMode = "auto";
if (displayMode == "auto")
displayMode = this.autoDisplayMode;
document.documentElement.dataset.displayMode = displayMode;
};
get autoDisplayMode() {
let insets = helpers.html.getSafeAreaInsets();
if (ppixiv.ios && navigator.platform.indexOf("iPhone") != -1) {
if (insets.bottom > 0)
return "notch";
if (!navigator.standalone && insets.left > 20 && insets.right == insets.left)
return "notch";
}
return "normal";
}
get currentDataSource() {
return this._dataSource;
}
async setCurrentDataSource(args) {
let token = this._setCurrentDataSourceToken = new Object();
while (this._setCurrentDataSourcePromise != null)
await this._setCurrentDataSourcePromise;
if (token !== this._setCurrentDataSourceToken)
return;
let promise = this._setCurrentDataSourcePromise = this._setCurrentDataSource(args);
promise.finally(() => {
if (promise == this._setCurrentDataSourcePromise)
this._setCurrentDataSourcePromise = null;
});
return promise;
}
async _setCurrentDataSource({
cause,
refresh,
scrollToTop,
startAtBeginning
}) {
let args = helpers.args.location;
let dataSource = ppixiv.site.createDataSourceForUrl(ppixiv.plocation, {
force: refresh,
startAtBeginning
});
let newScreenName = args.hash.get("view") ?? dataSource.defaultScreen;
console.assert(
newScreenName == "illust" || newScreenName == "search",
newScreenName
);
let newScreen = newScreenName == "illust" ? this._screenIllust : this._screenSearch;
let oldScreen = this._currentScreen;
let mediaId = null;
if (newScreen.screenType == "illust")
mediaId = dataSource.getUrlMediaId(args);
if (startAtBeginning) {
delete args.state.scroll;
dataSource.setStartPage(args, 1);
helpers.navigate(args, {
addToHistory: false,
cause: "refresh-data-source",
sendPopstate: false
});
}
let targetMediaId = null;
if (!scrollToTop) {
if (newScreen.screenType == "search") {
if (oldScreen?.screenType == "illust") {
targetMediaId = oldScreen?.displayedMediaId;
} else {
targetMediaId = newScreen.getTargetMediaId(args);
}
} else if (newScreen.screenType == "illust") {
targetMediaId = mediaId;
}
}
await dataSource.init({ targetMediaId });
if (newScreen.screenType == "illust" && args.hash.get("slideshow") == "first") {
mediaId = await this.getMediaIdForSlideshow({ dataSource });
if (mediaId == null) {
ppixiv.message.show("Couldn't find a slideshow image to view");
let args2 = helpers.args.location;
args2.hash.set("view", "search");
args2.hash.delete("slideshow");
helpers.navigate(args2, {
addToHistory: true,
cause: "slideshow-failed"
});
return;
}
console.log("Starting slideshow at:", mediaId);
args.hash.set("slideshow", "1");
dataSource.setUrlMediaId(mediaId, args);
helpers.navigate(args, {
addToHistory: false,
cause: "start-slideshow",
sendPopstate: false
});
}
if (this._dataSource != dataSource) {
if (this._dataSource != null) {
this._dataSource.shutdown();
if (this._dataSource.transient)
ppixiv.site.discardDataSource(this._dataSource);
}
this._dataSource = dataSource;
if (this._dataSource != null)
this._dataSource.startup();
}
if (newScreen.screenType === "search")
this._ignoreClicksUntil = Date.now() + 100;
console.debug(
\`Showing screen: \${newScreen.screenType}, data source: \${this._dataSource.name}, cause: \${cause}, media ID: \${mediaId ?? "(none)"}, scroll to: \${targetMediaId}\`
);
this._currentScreen = newScreen;
if (newScreen !== oldScreen) {
if (oldScreen)
oldScreen.visible = false;
if (newScreen)
newScreen.visible = true;
let e = new Event("screenchanged");
e.newScreen = newScreen.screenType;
window.dispatchEvent(e);
}
newScreen.setDataSource(dataSource, { targetMediaId });
if (this._contextMenu) {
this._contextMenu.setDataSource(this._dataSource);
let displayedMediaId = mediaId;
displayedMediaId ??= newScreen.displayedMediaId;
this._contextMenu.setMediaId(displayedMediaId);
}
ppixiv.imageTranslations.setDisplayedMediaId(mediaId);
let restoreHistory = cause == "initialization" || cause == "history" || cause == "leaving-virtual";
await newScreen.activate({
mediaId,
cause,
restoreHistory
});
if (oldScreen != null && oldScreen != newScreen)
oldScreen.deactivate();
}
getRectForMediaId(mediaId) {
return this._screenSearch.getRectForMediaId(mediaId);
}
getMediaURL(mediaId, { screen = "illust", tempView = false } = {}) {
console.assert(mediaId != null, "Invalid illust_id", mediaId);
let args = helpers.args.location;
if (helpers.mediaId.isLocal(mediaId)) {
if (helpers.mediaId.parse(mediaId).type == "folder") {
screen = "search";
args = new helpers.args("/");
}
}
let { type, id } = helpers.mediaId.parse(mediaId);
if (type == "user")
return new helpers.args(\`/users/\${id}/artworks#ppixiv\`);
let oldMediaId = this._dataSource.getUrlMediaId(args);
this._setActiveScreenInUrl(args, screen);
this._dataSource.setUrlMediaId(mediaId, args);
if (tempView) {
args.hash.set("virtual", "1");
args.hash.set("temp-view", "1");
} else {
args.hash.delete("virtual");
args.hash.delete("temp-view");
}
let [illustId] = helpers.mediaId.toIllustIdAndPage(mediaId);
let [oldIllustId] = helpers.mediaId.toIllustIdAndPage(oldMediaId);
if (illustId != oldIllustId)
args.hash.delete("view-muted");
return args;
}
showMediaId(mediaId, { addToHistory = false, ...options } = {}) {
let args = this.getMediaURL(mediaId, options);
helpers.navigate(args, { addToHistory });
}
getDisplayedScreen() {
return this._currentScreen?.screenType;
}
get displayedMediaId() {
if (this._currentScreen?.screenType != "illust" || this._dataSource == null)
return null;
let args = helpers.args.location;
return this._dataSource.getUrlMediaId(args);
}
_setActiveScreenInUrl(args, screen) {
if (screen == this._dataSource.defaultScreen)
args.hash.delete("view");
else
args.hash.set("view", screen);
if (screen == "search") {
args.hash.delete("page");
args.hash.delete("illust_id");
}
if (screen != "illust")
delete args.state.zoom;
}
scrollSearchToMediaId(dataSource, mediaId) {
if (this._currentScreen.screenType == "search")
return;
this._screenSearch.setDataSource(dataSource, { targetMediaId: mediaId });
}
navigateFromIllustToSearch(args) {
if (!ppixiv.phistory.permanent) {
helpers.navigate(args);
return;
}
let previousUrl = ppixiv.phistory.previousStateUrl;
let canonicalPreviousUrl = previousUrl ? helpers.getCanonicalUrl(previousUrl) : null;
let canonicalNewUrl = helpers.getCanonicalUrl(args.url);
let sameUrl = helpers.areUrlsEquivalent(
canonicalPreviousUrl,
canonicalNewUrl
);
if (sameUrl) {
console.log("Navigated search is last in history, going there instead");
ppixiv.phistory.back();
} else {
helpers.navigate(args, { addToHistory: false });
}
}
_windowClickCapture = (e) => {
if (e.button != 0 || e.metaKey || e.ctrlKey || e.altKey)
return;
if (!(e.target instanceof Element))
return;
if (e.defaultPrevented)
return;
let a = e.target.closest("A");
if (a == null || !a.hasAttribute("href"))
return;
let url = new URL(a.href, document.href);
if (!helpers.args.isPPixivUrl(url))
return;
e.preventDefault();
e.stopImmediatePropagation();
if (ppixiv.ios && this._ignoreClicksUntil != null && Date.now() < this._ignoreClicksUntil) {
console.log(
\`Ignoring click while activating screen: \${this._ignoreClicksUntil - Date.now()}\`
);
return;
}
url = helpers.pixiv.getUrlWithoutLanguage(url);
let { mediaId } = this.getMediaIdAtElement(a);
if (mediaId) {
let args = new helpers.args(a.href);
let screen = args.hash.has("view") ? args.hash.get("view") : "illust";
this.showMediaId(mediaId, {
screen,
addToHistory: true
});
return;
}
helpers.navigate(url, {
scrollToTop: a.dataset.scrollToTop
});
};
_redirectEventToScreen = (e) => {
let screen = this._currentScreen;
if (screen == null)
return;
if (DialogWidget.activeDialogs.length > 0)
return;
if (helpers.html.isAbove(screen.root, e.target))
return;
let e2 = new e.constructor(e.type, e);
if (!screen.root.dispatchEvent(e2)) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
};
_windowKeydown = (e) => {
let screen = this._currentScreen;
if (screen == null)
return;
if (DialogWidget.activeDialogs.length > 0)
return;
screen.handleKeydown(e);
};
getMediaIdAtElement(element) {
if (element == null)
return {};
let mediaElement = element.closest("[data-media-id]");
if (mediaElement)
return { mediaId: mediaElement.dataset.mediaId };
let userElement = element.closest("[data-user-id]");
if (userElement)
return { mediaId: \`user:\${userElement.dataset.userId}\` };
return {};
}
loadResourceBlobs() {
ppixiv.resources = {};
for (let [path, value] of Object.entries(getResources())) {
let filename = new URL(path, ppixiv.plocation).pathname;
let binary = filename.endsWith(".png") || filename.endsWith(".woff");
if (!binary) {
ppixiv.resources[path] = value;
continue;
}
let blob = new Blob([value]);
let url = URL.createObjectURL(blob);
ppixiv.resources[path] = url;
}
}
_temporarilyHideDocument() {
if (document.documentElement == null)
return;
document.documentElement.style.filter = "brightness(0)";
document.documentElement.style.backgroundColor = "#000";
}
_undoTemporarilyHideDocument() {
document.documentElement.style.filter = "";
document.documentElement.style.backgroundColor = "";
}
toggleSlideshow() {
if (this._currentScreen.screenType != "illust")
return;
let args = helpers.args.location;
let enabled = args.hash.get("slideshow") == "1";
if (enabled)
args.hash.delete("slideshow");
else
args.hash.set("slideshow", "1");
helpers.navigate(args, { addToHistory: false, cause: "toggle slideshow" });
}
get slideshowMode() {
return helpers.args.location.hash.get("slideshow");
}
loopSlideshow() {
if (this._currentScreen.screenType != "illust")
return;
let args = helpers.args.location;
let enabled = args.hash.get("slideshow") == "loop";
if (enabled)
args.hash.delete("slideshow");
else
args.hash.set("slideshow", "loop");
helpers.navigate(args, { addToHistory: false, cause: "loop" });
}
get slideshowURL() {
let args = this._dataSource.args;
args.hash.set("slideshow", "first");
args.hash.set("view", "illust");
return args;
}
async getMediaIdForSlideshow({ dataSource }) {
await dataSource.loadPage(dataSource.initialPage);
let mediaId = dataSource.idList.getFirstId();
if (mediaId == null)
return null;
let { type } = helpers.mediaId.parse(mediaId);
if (type != "file" && type != "illust") {
console.log("Can't display ID as slideshow:", mediaId);
return null;
}
return mediaId;
}
addClicksToSearchHistory(node) {
node.addEventListener("click", function(e) {
if (e.defaultPrevented)
return;
if (e.target.tagName != "A" || !e.target.hasAttribute("href"))
return;
let url = new URL(e.target.href);
url = helpers.pixiv.getUrlWithoutLanguage(url);
let parts = url.pathname.split("/");
let firstPart = parts[1];
if (firstPart != "tags")
return;
let tag = helpers.pixiv.getSearchTagsFromUrl(url);
SavedSearchTags.add(tag);
});
}
};
/\x2f web/vview/app-startup.js
var AppStartupNative = class {
constructor() {
let ios = navigator.platform.indexOf("iPhone") != -1 || navigator.platform.indexOf("iPad") != -1;
let android = navigator.userAgent.indexOf("Android") != -1;
let mobile = ios || android;
window.ppixiv = {
native: true,
mobile,
ios,
android
};
console.log(\`vview setup: \${""}\`);
console.log("Browser:", navigator.userAgent);
this._cleanupEnvironment();
console.log("Launching app");
new App({});
}
_cleanupEnvironment() {
window.Document.prototype.realCreateElement = window.Document.prototype.createElement;
window.realRequestAnimationFrame = window.requestAnimationFrame.bind(window);
window.realCancelAnimationFrame = window.cancelAnimationFrame.bind(window);
window.realSetTimeout = window.setTimeout.bind(window);
window.realClearTimeout = window.clearTimeout.bind(window);
window.realSetInterval = window.setInterval.bind(window);
window.realClearInterval = window.clearInterval.bind(window);
window.realImage = window.Image;
window.realFetch = window.fetch;
window.MessagePort.prototype.realPostMessage = window.MessagePort.prototype.postMessage;
}
};
var AppStartup = class {
constructor() {
this.initialSetup();
}
async initialSetup() {
let native = location.hostname != "pixiv.net" && location.hostname != "www.pixiv.net";
if (native) {
new AppStartupNative();
return;
}
let ios = navigator.platform.indexOf("iPhone") != -1 || navigator.platform.indexOf("iPad") != -1;
let android = navigator.userAgent.indexOf("Android") != -1;
let mobile = ios || android;
if (window.ppixiv) {
console.error(
"ppixiv has been loaded twice. Is it loaded in multiple script managers?"
);
return;
}
window.ppixiv = {
native: false,
mobile,
ios,
android
};
console.debug("Browser:", navigator.userAgent);
for (let node of document.querySelectorAll("script[id *= 'Stay']"))
node.remove();
this.active = this._activeForCurrentUrl();
window.addEventListener("popstate", (e) => this._windowPopstate(e), {
capture: true
});
if (!this.active) {
this.setupDisabledUi();
return;
}
document.documentElement.style.backgroundColor = "#000";
this._cleanupEnvironment();
this.suppressingErrors = true;
await this._waitForContentLoaded();
this.suppressingErrors = false;
window.ppixivShowLoggedOut = this.showLoggedOutMessage.bind(this);
new App();
}
_waitForContentLoaded() {
return new Promise((accept, reject) => {
if (document.readyState != "loading") {
accept();
return;
}
window.addEventListener("DOMContentLoaded", (e) => accept(), {
capture: true,
once: true
});
});
}
async setupDisabledUi(loggedOut = false) {
console.log("ppixiv is currently disabled");
if (!loggedOut && ppixiv.mobile)
return;
await this._waitForContentLoaded();
let activateIconUrl = URL.createObjectURL(new Blob([favicon_default]));
let disabledUi = document.createElement("div");
disabledUi.innerHTML = \`
<div class=ppixiv-disabled-ui>
<!-- The top-level template must contain only one node and we only create one
of these, so we just put this style in here. -->
<style>
.ppixiv-disabled-ui {
position: fixed;
bottom: 10px;
left: 16px;
z-index: 10;
}
.ppixiv-disabled-ui > a {
border: none;
display: block;
width: 46px;
height: 44px;
cursor: pointer;
background-color: transparent;
opacity: 0.7;
text-decoration: none;
}
.ppixiv-disabled-ui > a:hover {
opacity: 1;
}
</style>
<a href="#ppixiv">
<img src=\${activateIconUrl}>
</a>
</div>
\`;
disabledUi = disabledUi.firstElementChild;
this.refreshDisabledUi(disabledUi);
document.body.appendChild(disabledUi);
disabledUi.addEventListener(
"focus",
(e) => this.refreshDisabledUi(disabledUi),
{ capture: true }
);
window.addEventListener(
"pp:popstate",
(e) => this.refreshDisabledUi(disabledUi),
{ capture: true }
);
if (this._urlSupported(window.location)) {
this._storeDisabled(true);
}
if (loggedOut) {
disabledUi.querySelector("a").addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
this.showLoggedOutMessage(true);
});
}
}
showLoggedOutMessage(force) {
let lastShown = window.sessionStorage.showedLogoutMessage || 0;
let timeSinceShown = Date.now() - lastShown;
let hoursSinceShown = timeSinceShown / (60 * 60 * 1e3);
if (!force && hoursSinceShown < 6)
return;
window.sessionStorage.showedLogoutMessage = Date.now();
alert("Please log in to use ppixiv.");
}
refreshDisabledUi(disabledUi) {
if (this._urlSupported(window.location)) {
let url = new URL(window.location);
disabledUi.querySelector("a").href = url;
} else {
disabledUi.querySelector("a").href = "/ranking.php?mode=daily#ppixiv";
}
}
_activeForCurrentUrl() {
if (ppixiv.native)
return true;
if (window.location.hash === "")
return this._activeByDefault();
if (!window.location.hash.startsWith("#ppixiv"))
return false;
return this._urlSupported(window.location);
}
_windowPopstate = (e) => {
const currently_active = this._activeForCurrentUrl();
if (this.active === currently_active)
return;
e.stopImmediatePropagation();
if (this.active === currently_active)
return;
this._storeDisabled(!currently_active);
console.log("Active state changed");
document.location.reload();
};
_storeDisabled(disabled) {
if (disabled)
window.sessionStorage.ppixiv_disabled = 1;
else
delete window.sessionStorage.ppixiv_disabled;
}
_activeByDefault() {
if (ppixiv.native)
return true;
let disabled_by_default = localStorage["_ppixiv_disabled-by-default"] == "true";
if (disabled_by_default)
return false;
if (window.sessionStorage.ppixiv_disabled)
return false;
let pathname = this._getPathWithoutLanguage(window.location.pathname);
if (pathname == "/")
return true;
return this._urlSupported(window.location);
}
_getPathWithoutLanguage(path) {
if (/^\\/..\\/\x2f.exec(path))
return path.substr(3);
else
return path;
}
_urlSupported(url) {
if (ppixiv.native)
return true;
url = new URL(url);
const pathname = this._getPathWithoutLanguage(url.pathname);
const parts = pathname.split("/");
const firstPart = parts[1];
if (firstPart === "artworks")
return true;
if (firstPart === "user" && parts[3] === "series")
return true;
if (firstPart === "users")
return true;
if (pathname === "/new_illust.php" || pathname === "/new_illust_r18.php")
return true;
if (pathname === "/bookmark_new_illust.php" || pathname === "/bookmark_new_illust_r18.php")
return true;
if (firstPart === "tags")
return true;
if (pathname === "/discovery")
return true;
if (pathname === "/discovery/users")
return true;
if (pathname === "/bookmark_detail.php")
return true;
if (pathname === "/ranking.php")
return true;
if (pathname === "/search_user.php")
return true;
if (pathname.startsWith("/request/complete"))
return true;
if (firstPart === "" && window.location.hash.startsWith("#ppixiv/edits"))
return true;
return false;
}
_cleanupEnvironment() {
window.realRequestAnimationFrame = window.requestAnimationFrame.bind(window);
function isAllowed(type) {
let e = new Error();
let { stack } = e;
let allowedHosts = ["recaptcha.net", "www.gstatic.com/recaptcha"];
for (let host of allowedHosts) {
if (stack.indexOf(host) != -1) {
return true;
}
}
return false;
}
window.__SENTRY__ = {};
Object.freeze(window.__SENTRY__);
for (let key of [
"onerror",
"onunhandledrejection",
"_send",
"_time",
"webpackJsonp",
"touchJsonp"
]) {
if (key == "onerror" || key == "onunhandledrejection") {
window[key] = (message, source, lineno, colno, error) => {
let returnToSuppressError = key == "onerror";
if (!this.suppressingErrors)
return !returnToSuppressError;
else
return returnToSuppressError;
};
}
Object.defineProperty(window, key, {
get: function() {
return null;
},
set: function(value) {
}
});
}
function unwrapFunc(obj, name, { ignore_missing = false } = {}) {
let orig_func = obj.__proto__ && obj.__proto__[name] ? obj.__proto__[name] : obj[name];
if (!orig_func) {
if (!ignore_missing)
console.log("Couldn't find function to unwrap:", name);
return;
}
if (!orig_func.__sentry_original__)
return;
while (orig_func.__sentry_original__)
orig_func = orig_func.__sentry_original__;
obj[name] = orig_func;
}
try {
unwrapFunc(window, "fetch");
unwrapFunc(window, "setTimeout");
unwrapFunc(window, "setInterval");
unwrapFunc(window, "clearInterval");
unwrapFunc(window, "requestAnimationFrame");
unwrapFunc(window, "cancelAnimationFrame");
unwrapFunc(EventTarget.prototype, "addEventListener");
unwrapFunc(EventTarget.prototype, "removeEventListener");
unwrapFunc(XMLHttpRequest.prototype, "send");
} catch (e) {
console.error("Error unwrapping environment", e);
}
function deleteOverrides(obj) {
for (let prop of Object.getOwnPropertyNames(obj)) {
try {
delete obj[prop];
} catch (e) {
}
}
try {
Object.freeze(obj);
} catch (e) {
console.warn(\`Error freezing \${obj}: \${e}\`);
}
}
try {
Object.freeze(EventTarget.prototype);
deleteOverrides(window.history);
deleteOverrides(window.document);
for (let name of Object.keys(window.console))
unwrapFunc(console, name, { ignore_missing: true });
Object.freeze(window.console);
if ("jQuery" in window)
jQuery.Deferred.exceptionHook = () => {
};
} catch (e) {
console.error("Error unwrapping environment", e);
}
try {
window.MessagePort.prototype.realPostMessage = window.MessagePort.prototype.postMessage;
window.MessagePort.prototype.postMessage = function(...args) {
if (!isAllowed("postMessage"))
return -1;
return window.MessagePort.prototype.realPostMessage.apply(this, args);
};
} catch (e) {
console.error("Error disabling postMessage", e);
}
function blockFunction(obj, name, realName) {
let func = obj[name];
console.assert(func != null);
window[realName] = func;
window[name] = function(...args) {
if (!isAllowed(name))
return -1;
return func.apply(this, args);
};
}
blockFunction(window, "requestAnimationFrame", "realRequestAnimationFrame");
blockFunction(window, "cancelAnimationFrame", "realCancelAnimationFrame");
blockFunction(window, "setTimeout", "realSetTimeout");
blockFunction(window, "setInterval", "realSetInterval");
blockFunction(window, "clearTimeout", "realClearTimeout");
blockFunction(window, "clearInterval", "realClearInterval");
try {
window.addEventListener = Window.prototype.addEventListener.bind(window);
window.removeEventListener = Window.prototype.removeEventListener.bind(window);
} catch (e) {
}
window.realImage = window.Image;
window.Image = function() {
};
window.realFetch = window.fetch;
class dummy_fetch {
sent() {
return this;
}
}
dummy_fetch.prototype.ok = true;
window.fetch = function() {
return new dummy_fetch();
};
window.XMLHttpRequest = function() {
};
const origCreateElement = window.Document.prototype.createElement;
window.Document.prototype.realCreateElement = window.Document.prototype.createElement;
window.Document.prototype.createElement = function(type, options) {
if (type === "script" || type === "style" || type === "iframe") {
if (!isAllowed("createElement")) {
console.warn(\`Disabling createElement \${type}\`);
}
}
return origCreateElement.apply(this, arguments);
};
window.addEventListener(
"error",
(e) => {
let silence_error = false;
if (e.filename && e.filename.indexOf("s.pximg.net") != -1)
silence_error = true;
if (silence_error) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
},
true
);
window.addEventListener(
"unhandledrejection",
(e) => {
let silence_error = false;
if (e.reason && e.reason.stack && e.reason.stack.indexOf("s.pximg.net") != -1)
silence_error = true;
if (e.reason && e.reason.message == "Element disabled")
silence_error = true;
if (silence_error) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
},
true
);
}
};
new AppStartup();
})();
`;
// This is the entry point when running as a user script. bundle is the packaged
// app bundle. We'll run the app bundle in the page context.
//
// When running natively for vview, app-startup.js is launched directly and this isn't used.
async function Bootstrap({bundle}={})
{
// If this is an iframe, don't do anything, so we don't try to load in Pixiv iframes.
if(window.top != window.self)
return;
// Don't activate for things like sketch.pixiv.net.
if(window.location.hostname.endsWith(".pixiv.net") && window.location.hostname != "www.pixiv.net")
return;
// Some script managers define this on window, some as a local, and some not at all.
let info = typeof GM_info != "undefined"? GM_info:null;
console.log(`ppixiv is running in ${info?.scriptHandler} ${info?.version}`);
// If we're running in a user script and we have access to GM.xmlHttpRequest, give access to
// it to support saving image files to disk. Since we may be sandboxed, we do this through
// a MessagePort. We have to send this to the page, since the page has no way to send messages
// to us on its own.
//
// helpers.cleanup_environment disables postMessage. If we're not sandboxed, we'll be affected
// by this too, so save a copy of postMessage in the same way that it does.
window.MessagePort.prototype.xhrServerPostMessage = window.MessagePort.prototype.postMessage;
function createXhrHandler()
{
let { port1: clientPort, port2: serverPort } = new MessageChannel();
window.postMessage({ cmd: "download-setup" }, "*", [clientPort]);
serverPort.onmessage = (e) => {
let responsePort = e.ports[0];
let {
url,
method="GET",
formData,
responseType="arraybuffer",
headers=null,
} = e.data;
// console.log("GM.xmlHttpRequest request for:", url);
// If we were given a FormData in the form of an object, convert it to a
// FormData. For some reason FormData objects themselves can't be sent
// over a MessagePort.
let data = null;
if(formData)
{
data = new FormData();
for(let [key, value] of Object.entries(formData))
{
// The value might be a blob or an ArrayBuffer. Convert it to a blob.
//
// A bug in Firefox and/or FireMonkey causes the ArrayBuffer to be from the
// page context instead of the script context, which breaks "value instanceof ArrayBuffer".
// We can just not check, since constructing a blob from a blob doesn't hurt
// anything.
if(value.toString() == "[object ArrayBuffer]")
value = new Blob([value]);
data.append(key, value);
}
}
// Some script managers don't implement @connect and let user scripts access anything.
// Check the hostnames we give access to in case the script manager isn't.
url = new URL(url);
let allowedHosts = [
"i.pximg.net", "i-cf.pximg.net", "cotrans.touhou.ai"
];
let anyMatches = false;
for(let host of allowedHosts)
if(url.hostname.endsWith(host))
anyMatches = true;
if(!anyMatches)
{
responsePort.xhrServerPostMessage({ success: false, error: `Unexpected ppdownload URL: ${url}` });
return;
}
GM.xmlHttpRequest({
method, headers,
responseType,
// TamperMonkey takes a URL object, but ViolentMonkey throws an exception unless we
// convert to a string.
url: url.toString(),
data,
onload: (result) => {
let success = result.status < 400;
let error = `HTTP ${result.status}`;
let { response } = result;
// If the response is an ArrayBuffer, add it to the transfer list so we don't
// make a copy.
let transfer = [];
if(response instanceof ArrayBuffer)
transfer.push(response);
responsePort.xhrServerPostMessage({ success, error, response }, transfer);
},
// This API is broken and doesn't actually include any information about the error.
onerror: (e) => {
responsePort.xhrServerPostMessage({ success: false, error: "Request error" });
},
});
};
}
// Listen to requests from helpers._get_xhr_server.
window.addEventListener("request-download-channel", (e) => {
e.preventDefault();
createXhrHandler();
});
function runScript(source)
{
let script = document.createElement("script");
script.textContent = source;
document.documentElement.appendChild(script);
script.remove();
}
runScript(bundle);
}
// This script is executed by eval(), so this expression is its return value.
Bootstrap;
Bootstrap({bundle});
})();