// ==UserScript== // @name Manga OnlineViewer // @author Tago // @updateURL https://github.com/TagoDR/MangaOnlineViewer/raw/master/dist/Manga_OnlineViewer.meta.js // @downloadURL https://github.com/TagoDR/MangaOnlineViewer/raw/master/dist/Manga_OnlineViewer.user.js // @supportURL https://github.com/TagoDR/MangaOnlineViewer/issues // @namespace https://github.com/TagoDR // @description Shows all pages at once in online view for these sites: Asura Scans, Batoto, BilibiliComics, Comick, Comix.to, DemonicScans, Dynasty-Scans, Flame Comics, Ikigai Mangas - EltaNews, Ikigai Mangas - Ajaco, Kagane, KuManga, LeerCapitulo, LHTranslation, Local Files, M440, MangaBuddy, MangaDex, MangaFox, MangaHere, Mangago, MangaHub, MangaKakalot, NeloManga, MangaNato, NatoManga, MangaBats, MangaBall, MangaOni, MangaPark, MangaReader, MangaToons, MangaTown, ManhwaWeb, MangaGeko.com, MangaGeko.cc, NineAnime, Olympus, QiManhwa, ReadComicsOnline, ReaperScans, TuMangaOnline, WebNovel, WebToons, WeebCentral, WeebDex, Vortex Scans, ZeroScans, MangaStream WordPress Plugin, Realm Oasis, Voids-Scans, Luminous Scans, Shimada Scans, Night Scans, Manhwa-Freak, OzulScansEn, CypherScans, MangaGalaxy, LuaScans, Drake Scans, Rizzfables, NovatoScans, TresDaos, Lectormiau, NTRGod, Threedaos, FoOlSlide, Kireicake, Madara WordPress Plugin, MangaHaus, Isekai Scan, Comic Kiba, Zinmanga, mangatx, Toonily, Mngazuki, JaiminisBox, DisasterScans, ManhuaPlus, TopManhua, NovelMic, Reset-Scans, LeviatanScans, Dragon Tea, SetsuScans, ToonGod, Hades Scans // @version 2026.07.25.build-1335 // @license MIT // @icon https://cdn-icons-png.flaticon.com/32/2281/2281832.png // @run-at document-end // @grant unsafeWindow // @grant GM_getValue // @grant GM_setValue // @grant GM_listValues // @grant GM_deleteValue // @grant GM_xmlhttpRequest // @grant GM_addValueChangeListener // @noframes on // @connect * // @require https://cdn.jsdelivr.net/npm/colorjs.io@0.7.1/dist/color.global.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/jszip/3.9.1/jszip.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/nprogress/0.2.0/nprogress.min.js // @require https://cdn.jsdelivr.net/npm/lodash@4.17.23/lodash.min.js // @require https://cdn.jsdelivr.net/npm/hotkeys-js@4.0.2/dist/hotkeys-js.min.js // @require https://cdn.jsdelivr.net/npm/bowser@2.14.1/bundled.js // @require https://cdnjs.cloudflare.com/ajax/libs/blob-util/2.0.2/blob-util.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @include /https?:\/\/(www.)?(asurascans|asuracomics).(com|net)\/.+/ // @include /https?:\/\/(?:www\.)?(?:fto|jto|hto|dto|mto|wto|bato|battwo|batotwo|comiko|batocomic|readtoto|zbato|xbato|mangatoto)\.(?:to|com|net|org)\/(chapter|title).*/ // @include /https?:\/\/(www\.)?(bilibilicomics).net\/episode\/.+/ // @include /https?:\/\/(www\.)?comick.io\/.+/ // @include /https?:\/\/comix\.to\/(title|comic)\/.+\/.+/ // @include /https?:\/\/(www\.)?demonicscans\.org\/(title\/.+\/chapter\/.+|chaptered\.php\?manga=\d+&chapter=.+)/ // @include /https?:\/\/(www\.)?dynasty-scans.com\/chapters\/.+/ // @include /https?:\/\/(www.)?(flamecomics).(xyz)\/series\/.+/ // @include /https?:\/\/(visorikigai|visualikigai).(ajaco|eltanews|foodib|jobswu).(com|net|site)\/capitulo\/\d+/ // @include /https:\/\/(www\.)?kagane\.org\/series\/.+\/reader\/.+/ // @include /https?:\/\/(www\.)?kumanga.com\/manga\/leer\/.+/ // @include /https?:\/\/(www.)?leercapitulo.co\/leer\/.+/ // @include /https?:\/\/(www\.)?lhtranslation.net\/read.+/ // @include /(file:\/\/\/.+(index)?.html)/ // @include /https?:\/\/(www\.)?m440.in\/manga\/.+\/.+\/\d+/ // @include /https?:\/\/(www\.)?mangabuddy.com\/.+\/chapter.+/ // @include /https?:\/\/(www\.)?mangadex.org/ // @include /https?:\/\/(www\.)?(fanfox.net|mangahere.cc)\/manga\/.+\/.+\// // @include /https?:\/\/(www\.)?mangago.me\/.*\/.*\/.*/ // @include /https?:\/\/(www\.)?(mangahub).io\/chapter\/.+\/.+/ // @include /https?:\/\/(www\.)?(read|chap)?(nelomanga|mangakakalot|natomanga|manganato|mangabats|mangakakalove).(com|gg|net).*\/(chapter|manga)\/.+\/.+/ // @include /https?:\/\/mangaball\.net\/chapter-detail\/.+/ // @include /https?:\/\/(www\.)?manga-oni.com\/lector\/.+\/\d+\/cascada/ // @include /https?:\/\/(www\.)?(mangapark|mpark|comicpark|readpark|parkmanga).(com|me|org|net|io|to)\/title\/.+\/.+/ // @include /https?:\/\/(www\.)?mangareader.to\/read\/.+\/.+\/.+/ // @include /https?:\/\/.*mangatoon.mobi\/.+\/watch\/.+/ // @include /https?:\/\/www\.mangatown\.com\/manga\/.+\/.+\// // @include /https?:\/\/(www\.)?manhwaweb.com\/leer\/.+/ // @include /https?:\/\/(www\.)?mgeko.(com|cc)?\/reader\/.*/ // @include /https?:\/\/(www\.)?nineanime.com\/chapter\/.+/ // @include /https?:\/\/(www\.)?olympusxyz.com\/capitulo\/\d+\/.+/ // @include /https?:\/\/qimanhwa\.com\/series\/[^/]+\/chapter-.+/ // @include /https?:\/\/(www\.)?readcomicsonline.ru\/comic\/.*\/\d*/ // @include /https?:\/\/(www\.)?reaperscans\.com\/series\/.+\/chapter.+/ // @include /https?:\/\/(www\.)?zonatmo\.(com|org|app)\/(viewer|news|view_uploads|reader|library)\/.+/ // @include /https?:\/\/(www\.)?webnovel.com\/comic\/.+/ // @include /https?:\/\/(www\.)?webtoons.com\/.+viewer.+/ // @include /https?:\/\/(www\.)?(weebcentral).com\/chapters\/.+/ // @include /https?:\/\/(www\.)?weebdex\.org\/.+/ // @include /https?:\/\/(www.)?(vortexscans).(org)\/.+/ // @include /https?:\/\/(www\.)?zscans.com\/comics\/.+/ // @include /https?:\/\/[^/]*(scans?|comic|realm|rizz|hivetoon|tresdaos|zonamiau|ntrgod|threedaos)[^/]*\/.+/ // @include /^(?!.*jaiminisbox).*\/read\/.+/ // @include /https?:\/\/.+\/(manga|series|manhua|comic|ch|novel|webtoon|tmo)\/.+\/.+/ // @exclude /https?:\/\/(www\.)?tsumino.com\/.+/ // @exclude /https?:\/\/(www\.)?pururin.io\/.+/ // ==/UserScript== (function() { //#region \0rolldown/runtime.js var __defProp = Object.defineProperty; var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports); var __exportAll = (all, no_symbols) => { let target = {}; for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" }); return target; }; //#endregion //#region src/utils/tampermonkey.ts /** biome-ignore-all lint/suspicious/noExplicitAny: the values truly does not matter */ /** * Safely exposes a value to the window object, targeting `unsafeWindow` in userscript environments * and falling back to the standard `window` object. * @param {string} key - The key to assign the value to on the window object. * @param {any} content - The value to expose. */ function giveToWindow(key, content) { if (typeof unsafeWindow !== "undefined") unsafeWindow[key] = content; if (typeof window !== "undefined") window[key] = content; } /** * The primary logging function for the userscript, which prefixes all messages with a standard header. * @param {...any} text - The content to be logged. * @returns {any[]} The logged content. */ function logScript(...text) { console.log(`MangaOnlineViewer-main: `, ...text); return text; } /** * A verbose logging function that only outputs messages when in a development environment. * @param {...any} text - The content to be logged. * @returns {any[]} The logged content. */ function logScriptVerbose(...text) { if (["dev", "development"].includes("main")) console.info("MangaOnlineViewer: ", ...text); return text; } /** * A safe wrapper for `GM_deleteValue`. * @param {string} name - The key of the value to remove. */ function removeValueGM(name) { if (typeof GM_deleteValue !== "undefined") GM_deleteValue(name); else logScriptVerbose("Fake Removing: ", name); } /** * A shim for the `GM_info` object, providing fallback data for non-userscript environments. */ var getInfoGM = typeof GM_info !== "undefined" ? GM_info : { scriptHandler: "Console", script: { name: "Debug", version: "Testing" } }; /** * A safe wrapper for `GM_getValue`. * @param {string} name - The key of the value to retrieve. * @param {any} [defaultValue=null] - The default value to return if the key does not exist. * @returns {any} The retrieved value or the default. */ function getValueGM(name, defaultValue) { if (typeof GM_getValue !== "undefined") return GM_getValue(name, defaultValue); logScriptVerbose("Fake Getting: ", name, " = ", defaultValue); return defaultValue; } /** * Retrieves a value from storage and parses it as JSON. * @param {string} name - The key of the value to retrieve. * @param {any} [defaultValue=null] - The default value to return if the key does not exist. * @returns {any} The parsed JSON object or the default value. */ function getJsonGM(name, defaultValue) { const result = getValueGM(name, defaultValue); if (typeof result === "string" && result.trim() !== "") try { return JSON.parse(result); } catch (e) { logScript("Failed to parse JSON from storage", name, e); return defaultValue; } return result; } /** * Retrieves the global settings object from storage. * @param {ISettings} [defaultSettings] - The default settings to return if none are found. * @returns {Partial} The global settings object. */ function getGlobalSettings(defaultSettings) { return getJsonGM("settings", defaultSettings); } /** * Retrieves the site-specific (local) settings object from storage. * @param {ISettings} [defaultSettings] - The default settings to return if none are found. * @returns {Partial} The local settings object. */ function getLocalSettings(defaultSettings) { return getJsonGM(window.location.hostname, defaultSettings); } /** * A safe wrapper for `GM_setValue`. * @param {string} name - The key for the value to be stored. * @param {any} value - The value to store. * @returns {string} The string representation of the stored value. */ function setValueGM(name, value) { if (typeof GM_setValue !== "undefined") { GM_setValue(name, value); logScript("Setting: ", name, " = ", value); return value.toString(); } logScriptVerbose("Fake Setting: ", name, " = ", value); return String(value); } /** * Saves the global settings object to storage. * @param {Partial} value - The settings object to save. * @returns {string} The string representation of the saved object. */ function saveGlobalSettings(value) { return setValueGM("settings", value); } /** * Saves the site-specific (local) settings object to storage. * @param {Partial} value - The settings object to save. * @returns {string} The string representation of the saved object. */ function saveLocalSettings(value) { return setValueGM(window.location.hostname, value); } /** * Attempts to parse the browser name and version from the user agent string. * @returns {string} The browser name and version (e.g., "Chrome 108"). */ function getBrowser() { const result = bowser.getParser(window.navigator.userAgent).getBrowser(); return `${result.name} ${result.version}`; } /** * Gets the name of the userscript engine (e.g., 'Tampermonkey', 'Greasemonkey'). * @returns {string} The name of the script handler. */ function getEngine() { return getInfoGM.scriptHandler ?? "Greasemonkey"; } /** * Determines the type of device based on the user agent and screen size. * @returns {Device} The device type: 'mobile', 'tablet', or 'desktop'. */ var getDevice = () => { const device = bowser.getParser(window.navigator.userAgent).getPlatformType(true); if (device === "mobile" || window.matchMedia("screen and (max-width: 600px)").matches) return "mobile"; if (device === "tablet" || window.matchMedia("screen and (max-width: 992px)").matches) return "tablet"; return "desktop"; }; /** * A convenience function to check if the current device is a mobile or tablet. * @returns {boolean} `true` if the device is a mobile or tablet, `false` otherwise. */ var isMobile = () => getDevice() === "mobile" || getDevice() === "tablet"; /** * Checks if the script is running in standalone mode (single HTML file). * @returns {boolean} `true` if in standalone mode, `false` otherwise. */ var isStandalone = () => window.location.protocol === "file:" || window.location.pathname.endsWith("Manga_Local_Viewer.html"); /** * Sets up a listener for changes to a GM storage value, triggering a callback when the value is changed in another tab. * @param {(newSettings: Partial) => void} fn - The callback function to execute with the new settings. * @param {string} [gmValue='settings'] - The key of the GM value to listen to (e.g., 'settings' or a hostname). * @returns {number | undefined} The listener ID from `GM_addValueChangeListener`, or `undefined` if not available. */ var settingsChangeListener = (fn, gmValue = "settings") => { if (typeof GM_addValueChangeListener !== "undefined") try { return GM_addValueChangeListener(gmValue, (_name, _oldValue, newValue, remote) => { if (remote) fn(newValue); }); } catch (e) { logScript("Failed to add settings listener", e); } }; //#endregion //#region src/utils/checks.ts /** biome-ignore-all lint/suspicious/noExplicitAny: must be generic */ /** * Checks if a value is "empty". * An empty value is `null`, `undefined`, an empty string, an empty array, or an object with no own properties. * @example * isEmpty(null) // true * isEmpty(undefined) // true * isEmpty([]) // true * isEmpty({}) // true * isEmpty('') // true * isEmpty(false) // false * isEmpty(0) // false * isEmpty([{},{"0":false},{"":0},{"0":0}]) // false * isEmpty(42) // false * isEmpty([{"":1},{"0":1}]) // false * @param {any} value - The value to check. * @returns {boolean} `true` if the value is empty, `false` otherwise. */ function isEmpty(value) { return _.isEmpty(value) || _.isNil(value); } /** * Checks if a value is "nothing" by performing a deep check. * A "nothing" value includes falsy values (`null`, `undefined`, `false`, `0`, `''`), * empty arrays/objects, or arrays that recursively contain only "nothing" values. * Note: The check is recursive for arrays, but for objects, it only considers an object "nothing" if it is empty (`{}`). * @example * isNothing(null) // true * isNothing(undefined) // true * isNothing([]) // true * isNothing({}) // true * isNothing('') // true * isNothing(false) // true * isNothing(0) // true * isNothing([{}, [0, false, '']]) // true * isNothing(42) // false * isNothing([1]) // false * isNothing({ a: 0 }) // false * @param {any} value - The value to test. * @returns {boolean} `true` if the value is "nothing", `false` otherwise. */ function isNothing(value) { return isEmpty(value) || value === false || value === 0; } //#endregion //#region src/utils/waitFor.ts /** * Waits for an element matching the given selector to appear in the DOM. * @param {string} selector - The CSS selector for the desired element. * @param {HTMLElement} [target=document.body] - The parent element within which to observe for mutations. Defaults to `document.body`. * @returns {Promise} A promise that resolves with the found element once it is available. */ function waitForElm(selector, target = document.body) { return new Promise((resolve) => { const element = document.querySelector(selector); if (element) { resolve(element); return; } const observer = new MutationObserver(() => { const observedElement = document.querySelector(selector); if (observedElement) { resolve(observedElement); observer.disconnect(); } }); observer.observe(target, { childList: true, subtree: true, attributes: true }); }); } /** * Waits for a function to return a truthy value. * It repeatedly calls the function until it returns `true`. * @param {() => boolean} fn - A function that returns a boolean value indicating the desired condition. * @param {number} [timer=250] - The interval in milliseconds between checks. * @returns {Promise} A promise that resolves to `true` when the function returns a truthy value. */ function waitForFunc(fn, timer = 250) { return new Promise((resolve) => { const intervalId = setInterval(() => { if (fn()) { clearInterval(intervalId); resolve(true); } }, timer); }); } /** * Waits for a specific attribute to be present on an element and resolves with the attribute's value. * @param {string} selector - The CSS selector to identify the target element. * @param {string} attribute - The name of the attribute to wait for. * @param {Element} [target=document.body] - The parent element to observe for mutations. Defaults to `document.body`. * @returns {Promise} A promise that resolves with the value of the specified attribute. */ function waitForAtb(selector, attribute, target = document.body) { return new Promise((resolve) => { const element = target.querySelector(selector); if (element?.getAttribute(attribute)) { resolve(element.getAttribute(attribute) ?? ""); return; } const observer = new MutationObserver(() => { const observedElement = target.querySelector(selector); if (observedElement?.getAttribute(attribute)) { resolve(observedElement.getAttribute(attribute) ?? ""); observer.disconnect(); } }); observer.observe(target, { childList: true, subtree: true, attributes: true, attributeFilter: [attribute] }); }); } /** * Waits for a global variable to be available on the `unsafeWindow` object. * Note: This is specific to userscript environments like Tampermonkey. * @security This function uses `unsafeWindow` to access global variables on the page. This can be a security risk if the page's scripts are malicious. * @param {string | keyof typeof unsafeWindow} name - The name of the variable to wait for. * @param {HTMLElement} [target=document.body] - The DOM element to observe for changes as a fallback trigger. * @returns {Promise} A promise that resolves with the value of the variable once it becomes available. */ function waitForVar(name, target = document.body) { return new Promise((resolve) => { if (!isNothing(unsafeWindow[name])) { resolve(unsafeWindow[name]); return; } const observer = new MutationObserver(() => { if (!isNothing(unsafeWindow[name])) { resolve(unsafeWindow[name]); observer.disconnect(); } }); observer.observe(target, { childList: true, subtree: true, attributes: true }); }); } /** * Waits for a promise to resolve, but with a maximum timeout. * If the promise does not resolve within the timeout period, the race is lost and the returned promise rejects. * @template T * @param {Promise} promise - The promise to wait for. * @param {number} [timeout=5000] - The maximum time to wait in milliseconds. * @returns {Promise} A promise that resolves with the result of the input promise, or rejects if it times out. */ async function waitWithTimeout(promise, timeout = 5e3) { const timeoutPromise = new Promise((_, reject) => { setTimeout(() => reject(/* @__PURE__ */ new Error(`Timeout after ${timeout} ms`)), timeout); }); return Promise.race([promise, timeoutPromise]); } //#endregion //#region src/core/check.ts /** * A generic helper function to run a test condition. * @internal * @template T * @param {T | undefined} value - The value to check. If undefined, the test is skipped. * @param {() => Promise} waiter - A function that returns a promise that resolves when the condition is met. * @param {string} startLog - The message to log when the test starts. * @param {string} endLog - The message to log when the test ends. */ async function testCondition(value, waiter, startLog, endLog) { if (value !== void 0) { logScript(startLog); logScript(endLog, await waiter(value)); } } /** * Runs a series of checks for a given site to ensure that the page is ready for scraping. * @param {ISite} site - The site configuration object. */ async function runSiteTests(site) { await testCondition(site.waitAttr, (waitAttr) => waitForAtb(waitAttr?.[0], waitAttr?.[1]), `Waiting for Attribute ${site.waitAttr?.[1]} of ${site.waitAttr?.[0]}`, `Found Attribute ${site.waitAttr?.[1]} of ${site.waitAttr?.[0]} =`); await testCondition(site.waitEle, waitForElm, `Waiting for Element ${site.waitEle}`, "Found Element"); await testCondition(site.waitVar, waitForVar, `Waiting for Variable ${site.waitVar}`, "Found Variable"); await testCondition(site.waitFunc, waitForFunc, `Waiting to pass Function check ${site.waitFunc}`, "Found Function check"); await testCondition(site.waitTime, (waitTime) => new Promise((resolve) => setTimeout(resolve, waitTime)), `Waiting for ${site.waitTime} milliseconds`, "Continuing after timer"); } //#endregion //#region node_modules/nanostores/atom/index.js var listenerQueue = []; var lqIndex = 0; var batchSeen = null; var QUEUE_ITEMS_PER_LISTENER = 4; var nanostoresGlobal = globalThis.nanostoresGlobal ||= { epoch: 0 }; var drainQueue = () => { for (lqIndex = 0; lqIndex < listenerQueue.length; lqIndex += QUEUE_ITEMS_PER_LISTENER) listenerQueue[lqIndex](listenerQueue[lqIndex + 1].value, listenerQueue[lqIndex + 2], listenerQueue[lqIndex + 3]); listenerQueue.length = 0; }; var atom = /* @__NO_SIDE_EFFECTS__ */ (initialValue) => { let listeners = []; let $atom = { get() { if (!$atom.lc) $atom.listen(() => {})(); return $atom.value; }, init: initialValue, lc: 0, listen(listener) { $atom.lc = listeners.push(listener); return () => { for (let i = lqIndex + QUEUE_ITEMS_PER_LISTENER; i < listenerQueue.length;) if (listenerQueue[i] === listener) listenerQueue.splice(i, QUEUE_ITEMS_PER_LISTENER); else i += QUEUE_ITEMS_PER_LISTENER; let index = listeners.indexOf(listener); if (~index) { listeners.splice(index, 1); if (!--$atom.lc) $atom.off(); } }; }, notify(oldValue, changedKey) { nanostoresGlobal.epoch++; let runListenerQueue = !listenerQueue.length && !batchSeen; for (let listener of listeners) { if (batchSeen?.has(listener)) continue; batchSeen?.add(listener); listenerQueue.push(listener, $atom, oldValue, batchSeen ? void 0 : changedKey); } if (runListenerQueue) drainQueue(); }, off() {}, set(newValue) { let oldValue = $atom.value; if (oldValue !== newValue) { $atom.value = newValue; $atom.notify(oldValue); } }, subscribe(listener) { let unbind = $atom.listen(listener); listener($atom.value); return unbind; }, value: initialValue }; return $atom; }; //#endregion //#region node_modules/nanostores/lifecycle/index.js var MOUNT = 5; var UNMOUNT = 6; var REVERT_MUTATION = 10; var on = (object, listener, eventKey, mutateStore) => { object.events = object.events || {}; if (!object.events[eventKey + REVERT_MUTATION]) object.events[eventKey + REVERT_MUTATION] = mutateStore((eventProps) => { object.events[eventKey].reduceRight((event, l) => (l(event), event), { shared: {}, ...eventProps }); }); object.events[eventKey] = object.events[eventKey] || []; object.events[eventKey].push(listener); return () => { let currentListeners = object.events[eventKey]; let index = currentListeners.indexOf(listener); currentListeners.splice(index, 1); if (!currentListeners.length) { delete object.events[eventKey]; object.events[eventKey + REVERT_MUTATION](); delete object.events[eventKey + REVERT_MUTATION]; } }; }; var STORE_UNMOUNT_DELAY = 1e3; var onMount = ($store, initialize) => { let listener = (payload) => { let destroy = initialize(payload); if (destroy) $store.events[UNMOUNT].push(destroy); }; return on($store, listener, MOUNT, (runListeners) => { let originListen = $store.listen; $store.listen = (...args) => { if (!$store.lc && !$store.active) { $store.active = true; runListeners(); } return originListen(...args); }; let originOff = $store.off; $store.events[UNMOUNT] = []; $store.off = () => { originOff(); setTimeout(() => { if ($store.active && !$store.lc) { $store.active = false; for (let destroy of $store.events[UNMOUNT]) destroy(); $store.events[UNMOUNT] = []; } }, STORE_UNMOUNT_DELAY); }; return () => { $store.listen = originListen; $store.off = originOff; }; }); }; //#endregion //#region node_modules/nanostores/computed/index.js var computedStore = (stores, cb, batched) => { if (!Array.isArray(stores)) stores = [stores]; let previousArgs; let currentEpoch; let set = () => { if (currentEpoch === nanostoresGlobal.epoch) return; currentEpoch = nanostoresGlobal.epoch; let args = stores.map(($store) => $store.get()); if (!previousArgs || args.some((arg, i) => arg !== previousArgs[i])) { previousArgs = args; let value = cb(...args); if (value && value.then && value.t) value.then((asyncValue) => { if (previousArgs === args) $computed.set(asyncValue); }); else { $computed.set(value); currentEpoch = nanostoresGlobal.epoch; } } }; let $computed = /* @__PURE__ */ atom(void 0); let get = $computed.get; $computed.get = () => { set(); return get(); }; let timer; let run = batched ? () => { clearTimeout(timer); timer = setTimeout(set); } : set; onMount($computed, () => { let unbinds = stores.map(($store) => $store.listen(run)); set(); return () => { for (let unbind of unbinds) unbind(); }; }); return $computed; }; var computed = /* @__NO_SIDE_EFFECTS__ */ (stores, fn) => computedStore(stores, fn); //#endregion //#region node_modules/nanostores/map/index.js var map = /* @__NO_SIDE_EFFECTS__ */ (initial = {}) => { let $map = /* @__PURE__ */ atom(initial); $map.setKey = function(key, value) { let oldMap = $map.value; if (typeof value === "undefined" && key in $map.value) { $map.value = { ...$map.value }; delete $map.value[key]; $map.notify(oldMap, key); } else if ($map.value[key] !== value) { $map.value = { ...$map.value, [key]: value }; $map.notify(oldMap, key); } }; return $map; }; //#endregion //#region node_modules/lit-html/lit-html.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var t$5 = globalThis; var i$6 = (t) => t; var s$4 = t$5.trustedTypes; var e$9 = s$4 ? s$4.createPolicy("lit-html", { createHTML: (t) => t }) : void 0; var h$4 = "$lit$"; var o$12 = `lit$${Math.random().toFixed(9).slice(2)}$`; var n$7 = "?" + o$12; var r$7 = `<${n$7}>`; var l$2 = document; var c$4 = () => l$2.createComment(""); var a$1 = (t) => null === t || "object" != typeof t && "function" != typeof t; var u$2 = Array.isArray; var d$3 = (t) => u$2(t) || "function" == typeof t?.[Symbol.iterator]; var f$3 = "[ \n\f\r]"; var v$1 = /<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g; var _$1 = /-->/g; var m$1 = />/g; var p$2 = RegExp(`>|${f$3}(?:([^\\s"'>=/]+)(${f$3}*=${f$3}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`, "g"); var g = /'/g; var $ = /"/g; var y$1 = /^(?:script|style|textarea|title)$/i; var x = (t) => (i, ...s) => ({ _$litType$: t, strings: i, values: s }); var b$1 = x(1); var E = Symbol.for("lit-noChange"); var A = Symbol.for("lit-nothing"); var C = /* @__PURE__ */ new WeakMap(); var P = l$2.createTreeWalker(l$2, 129); function V(t, i) { if (!u$2(t) || !t.hasOwnProperty("raw")) throw Error("invalid template strings array"); return void 0 !== e$9 ? e$9.createHTML(i) : i; } var N = (t, i) => { const s = t.length - 1, e = []; let n, l = 2 === i ? "" : 3 === i ? "" : "", c = v$1; for (let i = 0; i < s; i++) { const s = t[i]; let a, u, d = -1, f = 0; for (; f < s.length && (c.lastIndex = f, u = c.exec(s), null !== u);) f = c.lastIndex, c === v$1 ? "!--" === u[1] ? c = _$1 : void 0 !== u[1] ? c = m$1 : void 0 !== u[2] ? (y$1.test(u[2]) && (n = RegExp("" === u[0] ? (c = n ?? v$1, d = -1) : void 0 === u[1] ? d = -2 : (d = c.lastIndex - u[2].length, a = u[1], c = void 0 === u[3] ? p$2 : "\"" === u[3] ? $ : g) : c === $ || c === g ? c = p$2 : c === _$1 || c === m$1 ? c = v$1 : (c = p$2, n = void 0); const x = c === p$2 && t[i + 1].startsWith("/>") ? " " : ""; l += c === v$1 ? s + r$7 : d >= 0 ? (e.push(a), s.slice(0, d) + h$4 + s.slice(d) + o$12 + x) : s + o$12 + (-2 === d ? i : x); } return [V(t, l + (t[s] || "") + (2 === i ? "" : 3 === i ? "" : "")), e]; }; var S$1 = class S$1 { constructor({ strings: t, _$litType$: i }, e) { let r; this.parts = []; let l = 0, a = 0; const u = t.length - 1, d = this.parts, [f, v] = N(t, i); if (this.el = S$1.createElement(f, e), P.currentNode = this.el.content, 2 === i || 3 === i) { const t = this.el.content.firstChild; t.replaceWith(...t.childNodes); } for (; null !== (r = P.nextNode()) && d.length < u;) { if (1 === r.nodeType) { if (r.hasAttributes()) for (const t of r.getAttributeNames()) if (t.endsWith(h$4)) { const i = v[a++], s = r.getAttribute(t).split(o$12), e = /([.?@])?(.*)/.exec(i); d.push({ type: 1, index: l, name: e[2], strings: s, ctor: "." === e[1] ? I : "?" === e[1] ? L : "@" === e[1] ? z : H }), r.removeAttribute(t); } else t.startsWith(o$12) && (d.push({ type: 6, index: l }), r.removeAttribute(t)); if (y$1.test(r.tagName)) { const t = r.textContent.split(o$12), i = t.length - 1; if (i > 0) { r.textContent = s$4 ? s$4.emptyScript : ""; for (let s = 0; s < i; s++) r.append(t[s], c$4()), P.nextNode(), d.push({ type: 2, index: ++l }); r.append(t[i], c$4()); } } } else if (8 === r.nodeType) if (r.data === n$7) d.push({ type: 2, index: l }); else { let t = -1; for (; -1 !== (t = r.data.indexOf(o$12, t + 1));) d.push({ type: 7, index: l }), t += o$12.length - 1; } l++; } } static createElement(t, i) { const s = l$2.createElement("template"); return s.innerHTML = t, s; } }; function M$1(t, i, s = t, e) { if (i === E) return i; let h = void 0 !== e ? s._$Co?.[e] : s._$Cl; const o = a$1(i) ? void 0 : i._$litDirective$; return h?.constructor !== o && (h?._$AO?.(!1), void 0 === o ? h = void 0 : (h = new o(t), h._$AT(t, s, e)), void 0 !== e ? (s._$Co ??= [])[e] = h : s._$Cl = h), void 0 !== h && (i = M$1(t, h._$AS(t, i.values), h, e)), i; } var R = class { constructor(t, i) { this._$AV = [], this._$AN = void 0, this._$AD = t, this._$AM = i; } get parentNode() { return this._$AM.parentNode; } get _$AU() { return this._$AM._$AU; } u(t) { const { el: { content: i }, parts: s } = this._$AD, e = (t?.creationScope ?? l$2).importNode(i, !0); P.currentNode = e; let h = P.nextNode(), o = 0, n = 0, r = s[0]; for (; void 0 !== r;) { if (o === r.index) { let i; 2 === r.type ? i = new k(h, h.nextSibling, this, t) : 1 === r.type ? i = new r.ctor(h, r.name, r.strings, this, t) : 6 === r.type && (i = new Z(h, this, t)), this._$AV.push(i), r = s[++n]; } o !== r?.index && (h = P.nextNode(), o++); } return P.currentNode = l$2, e; } p(t) { let i = 0; for (const s of this._$AV) void 0 !== s && (void 0 !== s.strings ? (s._$AI(t, s, i), i += s.strings.length - 2) : s._$AI(t[i])), i++; } }; var k = class k { get _$AU() { return this._$AM?._$AU ?? this._$Cv; } constructor(t, i, s, e) { this.type = 2, this._$AH = A, this._$AN = void 0, this._$AA = t, this._$AB = i, this._$AM = s, this.options = e, this._$Cv = e?.isConnected ?? !0; } get parentNode() { let t = this._$AA.parentNode; const i = this._$AM; return void 0 !== i && 11 === t?.nodeType && (t = i.parentNode), t; } get startNode() { return this._$AA; } get endNode() { return this._$AB; } _$AI(t, i = this) { t = M$1(this, t, i), a$1(t) ? t === A || null == t || "" === t ? (this._$AH !== A && this._$AR(), this._$AH = A) : t !== this._$AH && t !== E && this._(t) : void 0 !== t._$litType$ ? this.$(t) : void 0 !== t.nodeType ? this.T(t) : d$3(t) ? this.k(t) : this._(t); } O(t) { return this._$AA.parentNode.insertBefore(t, this._$AB); } T(t) { this._$AH !== t && (this._$AR(), this._$AH = this.O(t)); } _(t) { this._$AH !== A && a$1(this._$AH) ? this._$AA.nextSibling.data = t : this.T(l$2.createTextNode(t)), this._$AH = t; } $(t) { const { values: i, _$litType$: s } = t, e = "number" == typeof s ? this._$AC(t) : (void 0 === s.el && (s.el = S$1.createElement(V(s.h, s.h[0]), this.options)), s); if (this._$AH?._$AD === e) this._$AH.p(i); else { const t = new R(e, this), s = t.u(this.options); t.p(i), this.T(s), this._$AH = t; } } _$AC(t) { let i = C.get(t.strings); return void 0 === i && C.set(t.strings, i = new S$1(t)), i; } k(t) { u$2(this._$AH) || (this._$AH = [], this._$AR()); const i = this._$AH; let s, e = 0; for (const h of t) e === i.length ? i.push(s = new k(this.O(c$4()), this.O(c$4()), this, this.options)) : s = i[e], s._$AI(h), e++; e < i.length && (this._$AR(s && s._$AB.nextSibling, e), i.length = e); } _$AR(t = this._$AA.nextSibling, s) { for (this._$AP?.(!1, !0, s); t !== this._$AB;) { const s = i$6(t).nextSibling; i$6(t).remove(), t = s; } } setConnected(t) { void 0 === this._$AM && (this._$Cv = t, this._$AP?.(t)); } }; var H = class { get tagName() { return this.element.tagName; } get _$AU() { return this._$AM._$AU; } constructor(t, i, s, e, h) { this.type = 1, this._$AH = A, this._$AN = void 0, this.element = t, this.name = i, this._$AM = e, this.options = h, s.length > 2 || "" !== s[0] || "" !== s[1] ? (this._$AH = Array(s.length - 1).fill(/* @__PURE__ */ new String()), this.strings = s) : this._$AH = A; } _$AI(t, i = this, s, e) { const h = this.strings; let o = !1; if (void 0 === h) t = M$1(this, t, i, 0), o = !a$1(t) || t !== this._$AH && t !== E, o && (this._$AH = t); else { const e = t; let n, r; for (t = h[0], n = 0; n < h.length - 1; n++) r = M$1(this, e[s + n], i, n), r === E && (r = this._$AH[n]), o ||= !a$1(r) || r !== this._$AH[n], r === A ? t = A : t !== A && (t += (r ?? "") + h[n + 1]), this._$AH[n] = r; } o && !e && this.j(t); } j(t) { t === A ? this.element.removeAttribute(this.name) : this.element.setAttribute(this.name, t ?? ""); } }; var I = class extends H { constructor() { super(...arguments), this.type = 3; } j(t) { this.element[this.name] = t === A ? void 0 : t; } }; var L = class extends H { constructor() { super(...arguments), this.type = 4; } j(t) { this.element.toggleAttribute(this.name, !!t && t !== A); } }; var z = class extends H { constructor(t, i, s, e, h) { super(t, i, s, e, h), this.type = 5; } _$AI(t, i = this) { if ((t = M$1(this, t, i, 0) ?? A) === E) return; const s = this._$AH, e = t === A && s !== A || t.capture !== s.capture || t.once !== s.once || t.passive !== s.passive, h = t !== A && (s === A || e); e && this.element.removeEventListener(this.name, this, s), h && this.element.addEventListener(this.name, this, t), this._$AH = t; } handleEvent(t) { "function" == typeof this._$AH ? this._$AH.call(this.options?.host ?? this.element, t) : this._$AH.handleEvent(t); } }; var Z = class { constructor(t, i, s) { this.element = t, this.type = 6, this._$AN = void 0, this._$AM = i, this.options = s; } get _$AU() { return this._$AM._$AU; } _$AI(t) { M$1(this, t); } }; var j$1 = { M: h$4, P: o$12, A: n$7, C: 1, L: N, R, D: d$3, V: M$1, I: k, H, N: L, U: z, B: I, F: Z }; var B = t$5.litHtmlPolyfillSupport; B?.(S$1, k), (t$5.litHtmlVersions ??= []).push("3.3.3"); var D = (t, i, s) => { const e = s?.renderBefore ?? i; let h = e._$litPart$; if (void 0 === h) { const t = s?.renderBefore ?? null; e._$litPart$ = h = new k(i.insertBefore(c$4(), t), t, void 0, s ?? {}); } return h._$AI(t), h; }, { I: t$4 } = j$1, r$6 = (o) => void 0 === o.strings; /** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ //#endregion //#region node_modules/lit-html/directive.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var t$3 = { ATTRIBUTE: 1, CHILD: 2, PROPERTY: 3, BOOLEAN_ATTRIBUTE: 4, EVENT: 5, ELEMENT: 6 }; var e$7 = (t) => (...e) => ({ _$litDirective$: t, values: e }); var i$4 = class { constructor(t) {} get _$AU() { return this._$AM._$AU; } _$AT(t, e, i) { this._$Ct = t, this._$AM = e, this._$Ci = i; } _$AS(t, e) { return this.update(t, e); } update(t, e) { return this.render(...e); } }; //#endregion //#region node_modules/lit-html/async-directive.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var s$2 = (i, t) => { const e = i._$AN; if (void 0 === e) return !1; for (const i of e) i._$AO?.(t, !1), s$2(i, t); return !0; }; var o$11 = (i) => { let t, e; do { if (void 0 === (t = i._$AM)) break; e = t._$AN, e.delete(i), i = t; } while (0 === e?.size); }; var r$5 = (i) => { for (let t; t = i._$AM; i = t) { let e = t._$AN; if (void 0 === e) t._$AN = e = /* @__PURE__ */ new Set(); else if (e.has(i)) break; e.add(i), c$2(t); } }; function h$2(i) { void 0 !== this._$AN ? (o$11(this), this._$AM = i, r$5(this)) : this._$AM = i; } function n$5(i, t = !1, e = 0) { const r = this._$AH, h = this._$AN; if (void 0 !== h && 0 !== h.size) if (t) if (Array.isArray(r)) for (let i = e; i < r.length; i++) s$2(r[i], !1), o$11(r[i]); else null != r && (s$2(r, !1), o$11(r)); else s$2(this, i); } var c$2 = (i) => { i.type == t$3.CHILD && (i._$AP ??= n$5, i._$AQ ??= h$2); }; var f$1 = class extends i$4 { constructor() { super(...arguments), this._$AN = void 0; } _$AT(i, t, e) { super._$AT(i, t, e), r$5(this), this.isConnected = i._$AU; } _$AO(i, t = !0) { i !== this.isConnected && (this.isConnected = i, i ? this.reconnected?.() : this.disconnected?.()), t && (s$2(this, i), o$11(this)); } setValue(t) { if (r$6(this._$Ct)) this._$Ct._$AI(t, this); else { const i = [...this._$Ct._$AH]; i[this._$Ci] = t, this._$Ct._$AI(i, this, 0); } } disconnected() {} reconnected() {} }; //#endregion //#region node_modules/lit-html/directives/ref.js /** * @license * Copyright 2020 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var e$6 = () => new h$1(); var h$1 = class {}; var o$10 = /* @__PURE__ */ new WeakMap(); var n$4 = e$7(class extends f$1 { render(i) { return A; } update(i, [s]) { const e = s !== this.G; return e && this.rt(void 0), (e || this.lt !== this.ct) && (this.G = s, this.ht = i.options?.host, this.rt(this.ct = i.element)), A; } rt(t) { if (void 0 !== this.G) if (this.isConnected || (t = void 0), "function" == typeof this.G) { const i = this.ht ?? globalThis; let s = o$10.get(i); void 0 === s && (s = /* @__PURE__ */ new WeakMap(), o$10.set(i, s)), void 0 !== s.get(this.G) && this.G.call(this.ht, void 0), s.set(this.G, t), void 0 !== t && this.G.call(this.ht, t); } else this.G.value = t; } get lt() { return "function" == typeof this.G ? o$10.get(this.ht ?? globalThis)?.get(this.G) : this.G?.value; } disconnected() { this.lt === this.ct && this.rt(void 0); } reconnected() { this.rt(this.ct); } }); //#endregion //#region src/locales/index.ts /** * An array containing all available localizations for the application. * This is the central point for accessing different language packs. * @type {ILocale[]} */ var locales = [ { ID: "en_US", NAME: "English (US)", STARTING: "Starting Manga OnlineViewer", RESUME: "Resuming reading from Page ", WAITING: "Please wait, 3 seconds...", CHOOSE_BEGINNING: "Choose the Page to start from:", BUTTON_START: "Start Manga OnlineViewer", SETTINGS: "Settings", LANGUAGE: "Language", COLOR_SCHEME: "Color Scheme", THEME: "Theme", THEME_COLOR: "Color", THEME_HUE: "Color Hue", THEME_SHADE: "Color Shade", DEFAULT_LOAD_MODE: "Default Load Mode", LOAD_MODE_NORMAL: "Normal(Wait 3 sec)", LOAD_MODE_ALWAYS: "Always(Immediately)", LOAD_MODE_NEVER: "Never(Manually)", LOAD_SPEED: "Load Speed", DEFAULT_ZOOM: "Default Zoom (between 5 and 200)", DEFAULT_ZOOM_MODE: "Default Zoom Mode", MINIMUM_ZOOM: "Minimum Zoom relative to the width of screen (between 30 and 100)", ZOOM_STEP: "Zoom Change Step (between 5 and 50)", DEFAULT_VIEW_MODE: "Default View Mode", VIEW_MODE_VERTICAL: "Vertical", VIEW_MODE_LEFT: "Horizontal - Left to Right", VIEW_MODE_RIGHT: "Horizontal - Right to Left", VIEW_MODE_WEBCOMIC: "WebComic", VIEW_MODE_BOOK: "Book - Left to Right", VIEW_MODE_MANGA: "Manga - Right to Left", VIEW_MODE_GALLERY: "Gallery", FIT_WIDTH_OVERSIZED: "Fit Width if Oversized", SHOW_THUMBNAILS: "Show Thumbnails", HIDE_CONTROLS: "Always Hide Page Controls", HEADER_TYPE: "Change Header Type", HEADER_HOVER: "Hover", HEADER_SCROLL: "Scroll", HEADER_CLICK: "Click", HEADER_FIXED: "Fixed", HEADER_SIMPLE: "Simple", BUTTON_DOWNLOAD: "Download", DOWNLOAD_ZIP: "Download Zip file", DOWNLOAD_IMAGES: "Download Images as Zip Automatically", DOWNLOAD_PROGRESS: "Downloading: ##num## of ##total##", GENERATING_ZIP: "Generating Zip file...", DOWNLOAD_INCOMPLETE: "Download Incomplete", DOWNLOAD_INCOMPLETE_MESSAGE: "Some pages failed to download and were skipped. A list of failed pages has been added to the ZIP file.", BUTTON_NEXT: "Next", NEXT_CHAPTER: "Next Chapter", BUTTON_PREVIOUS: "Previous", PREVIOUS_CHAPTER: "Previous Chapter", BOOKMARKS: "Bookmarks", BOOKMARK: "Bookmark", BOOKMARK_REMOVED: "Bookmark Removed", BOOKMARK_SAVED: "Bookmark Saved", BOOKMARK_MESSAGE: "Next time you open this chapter it will resume from: Page ##num## (Only ONCE per Bookmark)", KEYBINDINGS: "Keybindings", EDIT_KEYBINDS: "Edit KeyBindings", SAVE_KEYBINDS: "Save KeyBindings", BUTTON_EDIT: "Edit", BUTTON_SAVE: "Save", KEYBIND_RULES: `

Supported Keys

Allowed modifiers: shift, option, alt, ctrl, control, command.
Special keys: backspace, tab, clear, enter, return, esc, escape, space, up, down, left, right, home, end, pageup, pagedown, del, delete, f1 - f19, num_0 - num_9, num_multiply, num_add, num_enter, num_subtract, num_decimal, num_divide.
Examples: a, ctrl+a , shift+a , num_2 , 2 `, ATTENTION: "Attention", WARNING: "Warning", BUTTON_RESET_SETTINGS: "Reset Settings", SETTINGS_RESET: "Settings have been reset, reload the page to take effect", LANGUAGE_CHANGED: "Language has been changed, reload the page to take effect", AUTO_DOWNLOAD: "Next time a chapter finish loading you will be prompted to save automatically", LAZY_LOAD: "Lazy load is incompatible with zip download, you will not be able to download with this setting ON.
Suggestion: Disable Thumbnails to save Bandwidth/Memory.", LAZY_LOAD_IMAGES_ENABLE: "Enable Lazy Load Images", LAZY_LOAD_IMAGES: "Lazy Start From Page (between 5 and 100)", RETURN_CHAPTER_LIST: "Return to Chapter List", PAGES_LOADED: "Pages Loaded", GO_TO_PAGE: "Go to Page", ENLARGE: "Enlarge", RESTORE: "Restore", REDUCE: "Reduce", FIT_WIDTH: "Fit Width", FIT_HEIGHT: "Fit Height", PERCENT: "Percent", TOGGLE_CONTROLS: "Toggle page controls", ZOOM_IN: "Zoom In", ZOOM_OUT: "Zoom Out", ZOOM_RESET: "Zoom Reset", ZOOM_WIDTH: "Zoom to Width", ZOOM_HEIGHT: "Zoom to Height", HIDE: "Hide", RELOAD: "Reload", SLOWLY: "Slowly", NORMAL: "Normal", FAST: "Fast", EXTREME: "Extreme", ALL_PAGES: "All Pages", SPEED_WARNING: "Loading Speed too High", SPEED_WARNING_MESSAGE: "This speed is not recommended.
It may hurt some servers or get your IP marked as DDoS attacker.
Please use with caution!", SCROLL_UP: "Scroll Up", SCROLL_DOWN: "Scroll Down", CLOSE: "Close", CANCEL: "Cancel", LIST_EMPTY: "List Empty", SCROLL_START: "Toggle Auto Scroll", INCREASE_SPEED: "Increase Scroll Speed", DECREASE_SPEED: "Decrease Scroll Speed", AUTO_SCROLL_HEIGHT: "Auto Scroll Speed in Pixels", VERTICAL_SEPARATOR: "Show Vertical Separators", END: "End", SCOPE: "Scope", GLOBAL: "Global", GENERAL: "General", LOADING: "Loading", ZOOM: "Zoom", OTHERS: "Others", NAVBAR_TYPE: "Change Navbar Type", NAVBAR_BOTTOM: "Bottom", NAVBAR_LEFT: "Left", NAVBAR_RIGHT: "Right", NAVBAR_DISABLED: "Disabled", PAGINATION_TYPE: "Pagination Type", PAGINATION_DISABLED: "Disabled", PAGINATION_SLIDER: "Slider", PAGINATION_ARROWS: "Side Arrows", PAGINATION_BOTH: "Both", FILE_MENU: "Main Menu", VIEW_MENU: "View Menu", ZOOM_MENU: "Zoom Menu", DOUBLE_PAGE: "Toggle Double Page", CHOOSE_FILE: "Choose File", NO_FILES_SELECTED: "No files selected" }, { ID: "es_ES", NAME: "Español (ES)", STARTING: "Iniciando Manga OnlineViewer", RESUME: "Continuando lectura desde la Página ", WAITING: "Por favor espere, 3 segundos...", CHOOSE_BEGINNING: "Elija la página en la que comenzar:", BUTTON_START: "Iniciar Manga OnlineViewer", SETTINGS: "Ajustes", LANGUAGE: "Idioma", COLOR_SCHEME: "Esquema de color", THEME: "Tema", THEME_COLOR: "Color", THEME_HUE: "Matiz del color", THEME_SHADE: "Saturación del color", DEFAULT_LOAD_MODE: "Modo de carga por defecto", LOAD_MODE_NORMAL: "Normal (Espera 3s)", LOAD_MODE_ALWAYS: "Siempre (Inmediatamente)", LOAD_MODE_NEVER: "Nunca (Manualmente)", LOAD_SPEED: "Velocidad carga", DEFAULT_ZOOM: "Zoom por defecto (entre 5 y 200)", DEFAULT_ZOOM_MODE: "Modo de zoom por defecto", MINIMUM_ZOOM: "Zoom mínimo relativo al ancho de la pantalla", ZOOM_STEP: "Paso entre cambios de zoom (entre 5 y 50)", DEFAULT_VIEW_MODE: "Modo de visualización por defecto", VIEW_MODE_VERTICAL: "Vertical", VIEW_MODE_LEFT: "Horizontal - Izquierda a derecha", VIEW_MODE_RIGHT: "Horizontal - Derecha a izquierda", VIEW_MODE_WEBCOMIC: "WebComic", VIEW_MODE_BOOK: "Libro - Izquierda a derecha", VIEW_MODE_MANGA: "Manga - Derecha a izquierda", VIEW_MODE_GALLERY: "Galería", FIT_WIDTH_OVERSIZED: "Ajustar ancho si es demasiado grande", SHOW_THUMBNAILS: "Mostrar miniaturas", HIDE_CONTROLS: "Ocultar siempre la barra de controles", HEADER_TYPE: "Cambiar tipo de cabecera", HEADER_HOVER: "Pasar por encima", HEADER_SCROLL: "Desplazamiento", HEADER_CLICK: "Hacer click", HEADER_FIXED: "Fijo", HEADER_SIMPLE: "Sencillo", BUTTON_DOWNLOAD: "Descargar", DOWNLOAD_ZIP: "Descargar fichero Zip", DOWNLOAD_IMAGES: "Autodescargar imágenes como Zip", DOWNLOAD_PROGRESS: "Descargando: ##num## de ##total##", GENERATING_ZIP: "Generando archivo Zip...", DOWNLOAD_INCOMPLETE: "Descarga Incompleta", DOWNLOAD_INCOMPLETE_MESSAGE: "Algunas páginas no se pudieron descargar y se saltaron. Se ha añadido una lista de páginas fallidas al archivo ZIP.", BUTTON_NEXT: "Siguiente", NEXT_CHAPTER: "Siguiente capítulo", BUTTON_PREVIOUS: "Anterior", PREVIOUS_CHAPTER: "Capítulo anterior", BOOKMARKS: "Marcadores", BOOKMARK: "Marcador", BOOKMARK_REMOVED: "Marcador eliminado", BOOKMARK_SAVED: "Marcador guardado", BOOKMARK_MESSAGE: "La próxima vez que abra este capítulo, continuará desde la página ##num## (Sólo UNA VEZ por Marcador)", KEYBINDINGS: "Atajos de teclado", EDIT_KEYBINDS: "Editar atajos", SAVE_KEYBINDS: "Guardar atajos", BUTTON_EDIT: "Editar", BUTTON_SAVE: "Guardar", KEYBIND_RULES: `

Teclas soportadas

Modificadores permitidos: shift, option, alt, ctrl, control, command.
Teclas especiales: backspace, tab, clear, enter, return, esc, escape, space, up, down, left, right, home, end, pageup, pagedown, del, delete, f1 - f19, num_0 - num_9, num_multiply, num_add, num_enter, num_subtract, num_decimal, num_divide.
Ejemplos: a, ctrl+a , shift+a , num_2 , 2 `, ATTENTION: "Atención", WARNING: "Alerta", BUTTON_RESET_SETTINGS: "Reiniciar ajustes(Reset Settings)", SETTINGS_RESET: "Se han restablecido los ajustes, vuelve a cargar la página para que surta efecto", LANGUAGE_CHANGED: "Se ha cambiado el idioma, vuelve a cargar la página para que surta efecto", AUTO_DOWNLOAD: "La próxima vez que termine de cargarse un capítulo, se le pedirá que guarde automáticamente", LAZY_LOAD: "La carga diferida es incompatible con la descarga zip, no podrá descargar con este ajuste activado.
Sugerencia: Desactivar miniaturas para ahorrar Ancho de banda/Memoria.", LAZY_LOAD_IMAGES_ENABLE: "Habilitar carga de imágenes diferida", LAZY_LOAD_IMAGES: "Empezar carga diferida a partir de la página (entre 5 y 100)", RETURN_CHAPTER_LIST: "Regresar a la lista de capítulos", PAGES_LOADED: "Páginas cargadas", GO_TO_PAGE: "Ir a página", ENLARGE: "Agrandar", RESTORE: "Restaurar", REDUCE: "Reducir", FIT_WIDTH: "Ajustar al ancho", FIT_HEIGHT: "Ajustar al alto", PERCENT: "Porcentual", TOGGLE_CONTROLS: "Alternar controles de página", ZOOM_IN: "Acercar", ZOOM_OUT: "Alejar", ZOOM_RESET: "Restablecer zoom", ZOOM_WIDTH: "Zoom al ancho", ZOOM_HEIGHT: "Zoom al alto", HIDE: "Ocultar", RELOAD: "Recargar", SLOWLY: "Lento", NORMAL: "Normal", FAST: "Rápido", EXTREME: "Extremo", ALL_PAGES: "Todas las páginas", SPEED_WARNING: "Velocidad de carga muy alta", SPEED_WARNING_MESSAGE: "No se recomienda esta velocidad.
Puede dañar algunos servidores o marcar su IP como atacante DDoS.
¡Utilícelo con precaución!", SCROLL_UP: "Desplazar arriba", SCROLL_DOWN: "Desplazarse hacia abajo", CLOSE: "Cerrar", CANCEL: "Cancelar", LIST_EMPTY: "Lista vacía", SCROLL_START: "Alternar desplazamiento automático", INCREASE_SPEED: "Aumentar la velocidad de desplazamiento", DECREASE_SPEED: "Disminuir la velocidad de desplazamiento", AUTO_SCROLL_HEIGHT: "Velocidad de desplazamiento automático en píxeles", VERTICAL_SEPARATOR: "Mostrar separadores verticales", END: "Fin", SCOPE: "Alcance", GLOBAL: "Global", GENERAL: "General", LOADING: "Carga", ZOOM: "Zoom", OTHERS: "Otros", NAVBAR_TYPE: "Cambiar el tipo de barra de navegación", NAVBAR_BOTTOM: "Abajo", NAVBAR_LEFT: "Izquierda", NAVBAR_RIGHT: "Derecha", NAVBAR_DISABLED: "Desactivado", PAGINATION_TYPE: "Tipo de paginación", PAGINATION_DISABLED: "Desactivado", PAGINATION_SLIDER: "Control deslizante", PAGINATION_ARROWS: "Flechas laterales", PAGINATION_BOTH: "Ambos", FILE_MENU: "Menú principal", VIEW_MENU: "Ver menú", ZOOM_MENU: "Menú Zoom", DOUBLE_PAGE: "Alternar Página Doble", CHOOSE_FILE: "Elegir archivo", NO_FILES_SELECTED: "No se han seleccionado archivos" }, { ID: "pt_BR", NAME: "Portugues (Brasil)", STARTING: "Iniciando Manga OnlineViewer", RESUME: "Continuando leitura na Pagina ", WAITING: "Por Favor espere, 3 segundos...", CHOOSE_BEGINNING: "Escolha a pagina de onde começar:", BUTTON_START: "Iniciar Manga OnlineViewer", SETTINGS: "Configurações", LANGUAGE: "Idioma", COLOR_SCHEME: "Esquema de Color", THEME: "Tema", THEME_COLOR: "Cor", THEME_HUE: "Tom da Cor", THEME_SHADE: "Saturação da Cor", DEFAULT_LOAD_MODE: "Forma de Carregamento Padrão", LOAD_MODE_NORMAL: "Normal(Esperando 3 sec)", LOAD_MODE_ALWAYS: "Sempre(Imediatamente)", LOAD_MODE_NEVER: "Nunca(Manualmente)", LOAD_SPEED: "Velocidade de Carregamento", DEFAULT_ZOOM: "Zoom padrão (entre 5 e 200)", DEFAULT_ZOOM_MODE: "Modo de Zoom padrão", MINIMUM_ZOOM: "Zoom minimo, relativo ao tamanho da tela (entre 30 e 100)", ZOOM_STEP: "Precisão da Mudança do Zoom (entre 5 e 50)", DEFAULT_VIEW_MODE: "Modo de Visualização Padrão", VIEW_MODE_VERTICAL: "Vertical", VIEW_MODE_LEFT: "Horizontal - Esquerda para Direita", VIEW_MODE_RIGHT: "Horizontal - Direita para Esquerda", VIEW_MODE_WEBCOMIC: "WebComic", VIEW_MODE_BOOK: "Livro - Esquerda para Direita", VIEW_MODE_MANGA: "Mangá - Direita para Esquerda", VIEW_MODE_GALLERY: "Galeria", FIT_WIDTH_OVERSIZED: "Encher a tela se grande demais", SHOW_THUMBNAILS: "Mostra Miniaturas", HIDE_CONTROLS: "Sempre esconder controles das paginas", HEADER_TYPE: "Mudar Tipo de Cabeçalho", HEADER_HOVER: "Passar por perto", HEADER_SCROLL: "Rolagem do Mouse", HEADER_CLICK: "Click", HEADER_FIXED: "Fixo", HEADER_SIMPLE: "Simples", BUTTON_DOWNLOAD: "Download", DOWNLOAD_ZIP: "Baixar arquivo Zip", DOWNLOAD_IMAGES: "Download das Imagens como Zip Automaticamente", DOWNLOAD_PROGRESS: "Baixando: ##num## de ##total##", GENERATING_ZIP: "Gerando arquivo Zip...", DOWNLOAD_INCOMPLETE: "Download Incompleto", DOWNLOAD_INCOMPLETE_MESSAGE: "Algumas páginas falharam ao baixar e foram puladas. Uma lista de páginas que falharam foi adicionada ao arquivo ZIP.", BUTTON_NEXT: "Proximo", NEXT_CHAPTER: "Proximo Capitulo", BUTTON_PREVIOUS: "Anterior", PREVIOUS_CHAPTER: "Capitulo Anterior", BOOKMARKS: "Marca paginas", BOOKMARK: "Marcar pagina", BOOKMARK_REMOVED: "Marca pagina Removido", BOOKMARK_SAVED: "Marca pagina Salvo", BOOKMARK_MESSAGE: "Proxima vez que abrir este capitulo continuará a partir da Pagina ##num## (Apenas UMA VEZ por marca pagina)", KEYBINDINGS: "Atalhos", EDIT_KEYBINDS: "Editar Atalhos", SAVE_KEYBINDS: "Salvar Atalhos", BUTTON_EDIT: "Editar", BUTTON_SAVE: "Salvar", KEYBIND_RULES: `

Teclas Suportadas

Modificadores permitidos: shift, option, alt, ctrl, control, command.
Teclas Especiais: backspace, tab, clear, enter, return, esc, escape, space, up, down, left, right, home, end, pageup, pagedown, del, delete, f1 - f19, num_0 - num_9, num_multiply, num_add, num_enter, num_subtract, num_decimal, num_divide.
Exemplos: a, ctrl+a , shift+a , num_2 , 2 `, ATTENTION: "Atenção", WARNING: "Alerta", BUTTON_RESET_SETTINGS: "Limpar Configurações(Reset Settings)", SETTINGS_RESET: "Configurações foram limpas, recarregue o site para efetivar a alteração", LANGUAGE_CHANGED: "Idioma foi alterado, recarregue o site para efetivar a alteração", AUTO_DOWNLOAD: "Proxima vez que abrir um capitulo download iniciara automaticamente", LAZY_LOAD: "Carregamento preguiçoso não é compativel com download de zip, não conseguira com essa configuração ativa.
Sugestão: Desative Miniaturas para economizar memoria e cota de internet.", LAZY_LOAD_IMAGES_ENABLE: "Ativar Carregamento de imagens preguiçoso", LAZY_LOAD_IMAGES: "Carregamento de paginas preguiçoso começa a partir de (entre 5 e 100)", RETURN_CHAPTER_LIST: "Voltar a lista de Capitulos", PAGES_LOADED: "Paginas Carregadas", GO_TO_PAGE: "Pular para", ENLARGE: "Aumentar", RESTORE: "Restaurar", REDUCE: "Diminuir", FIT_WIDTH: "Preencher Largura", FIT_HEIGHT: "Preencher Altura ", PERCENT: "Percentual", TOGGLE_CONTROLS: "Mostar controles de pagina", ZOOM_IN: "Mais Zoom", ZOOM_OUT: "Menos Zoom", ZOOM_RESET: "Resetar Zoom", ZOOM_WIDTH: "Zoom para Largura", ZOOM_HEIGHT: "Zoom para Altura", HIDE: "Esconder", RELOAD: "Recarregar", SLOWLY: "Devagar", NORMAL: "Normal", FAST: "Rapido", EXTREME: "Extremo", ALL_PAGES: "Todas as Paginas", SPEED_WARNING: "Velocidade de Carregamento muito alta", SPEED_WARNING_MESSAGE: "Essa velocidade não é recomendada.
Ela pode derrubar um servidor or marcar voce como um ataque hacker de DDoS.
Use com cuidado!", SCROLL_UP: "Subir Pagina", SCROLL_DOWN: "Descer Pagina", CLOSE: "Fechar", CANCEL: "Cancelar", LIST_EMPTY: "Lista Vazia", SCROLL_START: "Ativar Rolagem Automatica", INCREASE_SPEED: "Aumentar Valocidade da Rolagem", DECREASE_SPEED: "Diminuir Valocidade da Rolagem", AUTO_SCROLL_HEIGHT: "Velocidade da Rolagem Automatica em Pixels", VERTICAL_SEPARATOR: "Mostrar Separadores Verticais", END: "Fin", SCOPE: "Escopo", GLOBAL: "Global", GENERAL: "Geral", LOADING: "Carregamento", ZOOM: "Zoom", OTHERS: "Outros", NAVBAR_TYPE: "Mudar barra de navegação", NAVBAR_BOTTOM: "Embaixo", NAVBAR_LEFT: "Esquerda", NAVBAR_RIGHT: "Direita", NAVBAR_DISABLED: "Desativado", PAGINATION_TYPE: "Tipo de Paginação", PAGINATION_DISABLED: "Desativado", PAGINATION_SLIDER: "Controle deslizante", PAGINATION_ARROWS: "Setas Laterais", PAGINATION_BOTH: "Ambos", FILE_MENU: "Menu Principal", VIEW_MENU: "Menu de Visualizações", ZOOM_MENU: "Menu de Zoom", DOUBLE_PAGE: "Alternar Página Dupla", CHOOSE_FILE: "Escolher arquivo", NO_FILES_SELECTED: "Nenhum arquivo selecionado" }, { ID: "zh_CN", NAME: "中文 (简体)", STARTING: "正在启动 Manga OnlineViewer", RESUME: "从页面继续阅读 ", WAITING: "请等待3秒钟...", CHOOSE_BEGINNING: "选择要开始的页数:", BUTTON_START: "启动Manga OnlineViewer", SETTINGS: "设置", LANGUAGE: "语言", COLOR_SCHEME: "配色方案", THEME: "主题", THEME_COLOR: "颜色", THEME_HUE: "色相", THEME_SHADE: "色度", DEFAULT_LOAD_MODE: "默认加载模式", LOAD_MODE_NORMAL: "等待模式(等待3秒自动加载 )", LOAD_MODE_ALWAYS: "自动模式(无需等待)", LOAD_MODE_NEVER: "手动模式(点击启动)", LOAD_SPEED: "加载速度", DEFAULT_ZOOM: "默认缩放 (最小 5 最大 200)", DEFAULT_ZOOM_MODE: "默认缩放模式", MINIMUM_ZOOM: "相对于屏幕宽度的最小缩放 (最小 30 最大 100)", ZOOM_STEP: "缩放级别 (最小 5 最大 50)", DEFAULT_VIEW_MODE: "默认视图模式", VIEW_MODE_VERTICAL: "垂直有缝", VIEW_MODE_LEFT: "横向 - 从左到右", VIEW_MODE_RIGHT: "横向 - 从右到左", VIEW_MODE_WEBCOMIC: "垂直无缝", VIEW_MODE_BOOK: "书籍 - 从左到右", VIEW_MODE_MANGA: "漫画 - 从右到左", VIEW_MODE_GALLERY: "图库", FIT_WIDTH_OVERSIZED: "如果尺寸过大、则适合宽度", SHOW_THUMBNAILS: "显示缩略图", HIDE_CONTROLS: "始终隐藏页面控件", HEADER_TYPE: "更改标题显示方式", HEADER_HOVER: "悬停", HEADER_SCROLL: "滚动", HEADER_CLICK: "点击", HEADER_FIXED: "固定", HEADER_SIMPLE: "简单", BUTTON_DOWNLOAD: "下载", DOWNLOAD_ZIP: "下载压缩文件", DOWNLOAD_IMAGES: "自动将图片下载成ZIP", DOWNLOAD_PROGRESS: "正在下载:第 ##num## 页,共 ##total## 页", GENERATING_ZIP: "正在生成 Zip 文件...", DOWNLOAD_INCOMPLETE: "下载不完整", DOWNLOAD_INCOMPLETE_MESSAGE: "部分页面下载失败并已跳过。失败页面列表已添加到 ZIP 文件中。", BUTTON_NEXT: "下一页", NEXT_CHAPTER: "下一章", BUTTON_PREVIOUS: "上一页", PREVIOUS_CHAPTER: "上一章", BOOKMARKS: "书签", BOOKMARK: "Bookmark", BOOKMARK_REMOVED: "删除书签", BOOKMARK_SAVED: "保存书签", BOOKMARK_MESSAGE: "下次打开本章时,将从: 页码 ##num## (仅一次 每个书签)", KEYBINDINGS: "快捷键", EDIT_KEYBINDS: "编辑键绑定", SAVE_KEYBINDS: "保存键绑定", BUTTON_EDIT: "编辑", BUTTON_SAVE: "救", KEYBIND_RULES: `

支持的密钥

允许的修饰符: shift, option, alt, ctrl, control, command.
特殊键: backspace, tab, clear, enter, return, esc, escape, space, up, down, left, right, home, end, pageup, pagedown, del, delete, f1 - f19, num_0 - num_9, num_multiply, num_add, num_enter, num_subtract, num_decimal, num_divide.
例子: a, ctrl+a , shift+a , num_2 , 2 `, ATTENTION: "注意", WARNING: "警告", BUTTON_RESET_SETTINGS: "重置设置(Reset Settings)", SETTINGS_RESET: "设置已重置、重新加载页面才能生效", LANGUAGE_CHANGED: "语言已更改、重新加载页面才能生效", AUTO_DOWNLOAD: "下次章节加载完成时、系统将提示您自动保存", LAZY_LOAD: "延迟加载与zip下载不兼容、您将无法使用此设置下载.
建议: 禁用缩略图 以节省流量和内存.", LAZY_LOAD_IMAGES_ENABLE: "启用延迟加载图像", LAZY_LOAD_IMAGES: "惰性加载从页面 (最小 5 最大 100)", RETURN_CHAPTER_LIST: "返回章节列表", PAGES_LOADED: "已加载的页数", GO_TO_PAGE: "转到页数", ENLARGE: "放大", RESTORE: "还原", REDUCE: "缩小", FIT_WIDTH: "适合宽度", FIT_HEIGHT: "适合高度", PERCENT: "百分之", TOGGLE_CONTROLS: "显示隐藏页面控件", ZOOM_IN: "放大", ZOOM_OUT: "缩小", ZOOM_RESET: "还原", ZOOM_WIDTH: "适合宽度", ZOOM_HEIGHT: "适合高度", HIDE: "显示隐藏页面控件", RELOAD: "重新加载", SLOWLY: "慢速", NORMAL: "正常", FAST: "快速", EXTREME: "极端", ALL_PAGES: "所有页面", SPEED_WARNING: "加载速度过高", SPEED_WARNING_MESSAGE: "不建议使用此速度.
它可能会伤害某些服务器或将您的 IP 标记为 DDoS 攻击者.
请谨慎使用!", SCROLL_UP: "向上滚动", SCROLL_DOWN: "向下滚动", CLOSE: "关闭", CANCEL: "取消", LIST_EMPTY: "没有收藏书签", SCROLL_START: "切换自动滚动", INCREASE_SPEED: "增加滚动速度", DECREASE_SPEED: "降低滚动速度", AUTO_SCROLL_HEIGHT: "自动滚动速度(以像素为单位)", VERTICAL_SEPARATOR: "显示垂直分隔符", END: "结尾", SCOPE: "范围", GLOBAL: "全球", GENERAL: "常规", LOADING: "装载", ZOOM: "缩放", OTHERS: "别人", NAVBAR_TYPE: "更改导航栏类型", NAVBAR_BOTTOM: "底部", NAVBAR_LEFT: "左边", NAVBAR_RIGHT: "正确的", NAVBAR_DISABLED: "已禁用", PAGINATION_TYPE: "分页类型", PAGINATION_DISABLED: "已禁用", PAGINATION_SLIDER: "滑块", PAGINATION_ARROWS: "侧边箭头", PAGINATION_BOTH: "两者", FILE_MENU: "主菜单", VIEW_MENU: "查看菜单", ZOOM_MENU: "缩放菜单", DOUBLE_PAGE: "切换双页", CHOOSE_FILE: "选择文件", NO_FILES_SELECTED: "未选择任何文件" }, { ID: "de_DE", NAME: "Deutsch", STARTING: "Starte Manga OnlineViewer", RESUME: "Fortsetzen ab Seite ", WAITING: "Bitte warten, 3 Sekunden...", CHOOSE_BEGINNING: "Wähle die Startseite:", BUTTON_START: "Manga OnlineViewer starten", SETTINGS: "Einstellungen", LANGUAGE: "Sprache", COLOR_SCHEME: "Farbschema", THEME: "Design", THEME_COLOR: "Farbe", THEME_HUE: "Farbton", THEME_SHADE: "Schattierung", DEFAULT_LOAD_MODE: "Standard-Lademodus", LOAD_MODE_NORMAL: "Normal (3 Sek. warten)", LOAD_MODE_ALWAYS: "Immer (sofort)", LOAD_MODE_NEVER: "Nie (manuell)", LOAD_SPEED: "Ladegeschwindigkeit", DEFAULT_ZOOM: "Standard-Zoom (zwischen 5 und 200)", DEFAULT_ZOOM_MODE: "Standard-Zoommodus", MINIMUM_ZOOM: "Minimaler Zoom relativ zur Bildschirmbreite (zwischen 30 und 100)", ZOOM_STEP: "Zoom-Schrittgröße (zwischen 5 und 50)", DEFAULT_VIEW_MODE: "Standard-Ansichtsmodus", VIEW_MODE_VERTICAL: "Vertikal", VIEW_MODE_LEFT: "Horizontal - Links nach Rechts", VIEW_MODE_RIGHT: "Horizontal - Rechts nach Links", VIEW_MODE_WEBCOMIC: "WebComic", VIEW_MODE_BOOK: "Buch - Links nach Rechts", VIEW_MODE_MANGA: "Manga - Rechts nach Links", VIEW_MODE_GALLERY: "Galerie", FIT_WIDTH_OVERSIZED: "Breite anpassen bei Übergröße", SHOW_THUMBNAILS: "Miniaturansichten anzeigen", HIDE_CONTROLS: "Seitensteuerung immer ausblenden", HEADER_TYPE: "Kopfbereichstyp ändern", HEADER_HOVER: "Hover", HEADER_SCROLL: "Scrollen", HEADER_CLICK: "Klicken", HEADER_FIXED: "Fixiert", HEADER_SIMPLE: "Einfach", BUTTON_DOWNLOAD: "Herunterladen", DOWNLOAD_ZIP: "Zip-Datei herunterladen", DOWNLOAD_IMAGES: "Bilder automatisch als Zip herunterladen", DOWNLOAD_PROGRESS: "Herunterladen: ##num## von ##total##", GENERATING_ZIP: "Zip-Datei wird erstellt...", DOWNLOAD_INCOMPLETE: "Download unvollständig", DOWNLOAD_INCOMPLETE_MESSAGE: "Einige Seiten konnten nicht heruntergeladen werden und wurden übersprungen. Eine Liste der fehlgeschlagenen Seiten wurde der ZIP-Datei hinzugefügt.", BUTTON_NEXT: "Weiter", NEXT_CHAPTER: "Nächstes Kapitel", BUTTON_PREVIOUS: "Zurück", PREVIOUS_CHAPTER: "Vorheriges Kapitel", BOOKMARKS: "Lesezeichen", BOOKMARK: "Lesezeichen", BOOKMARK_REMOVED: "Lesezeichen entfernt", BOOKMARK_SAVED: "Lesezeichen gespeichert", BOOKMARK_MESSAGE: "Beim nächsten Öffnen dieses Kapitels wird ab fortgesetzt: Seite ##num## (Nur EINMAL pro Lesezeichen)", KEYBINDINGS: "Tastenkürzel", EDIT_KEYBINDS: "Tastenkürzel bearbeiten", SAVE_KEYBINDS: "Tastenkürzel speichern", BUTTON_EDIT: "Bearbeiten", BUTTON_SAVE: "Speichern", KEYBIND_RULES: `

Unterstützte Tasten

Erlaubte Modifikatoren: shift, option, alt, ctrl, control, command.
Spezielle Tasten: backspace, tab, clear, enter, return, esc, escape, space, up, down, left, right, home, end, pageup, pagedown, del, delete, f1 - f19, num_0 - num_9, num_multiply, num_add, num_enter, num_subtract, num_decimal, num_divide.
Beispiele: a, ctrl+a, shift+a, num_2, 2 `, ATTENTION: "Achtung", WARNING: "Warnung", BUTTON_RESET_SETTINGS: "Einstellungen zurücksetzen(Reset Settings)", SETTINGS_RESET: "Die Einstellungen wurden zurückgesetzt, bitte Seite neu laden", LANGUAGE_CHANGED: "Die Sprache wurde geändert, bitte Seite neu laden", AUTO_DOWNLOAD: "Beim nächsten Laden eines Kapitels wirst du automatisch gefragt, ob du speichern möchtest", LAZY_LOAD: "Lazy Load ist mit Zip-Download nicht kompatibel, mit dieser Einstellung kannst du nicht herunterladen.
Empfehlung: Miniaturansichten deaktivieren um Bandbreite/Speicher zu sparen.", LAZY_LOAD_IMAGES_ENABLE: "Lazy Load Bilder aktivieren", LAZY_LOAD_IMAGES: "Lazy Start ab Seite (zwischen 5 und 100)", RETURN_CHAPTER_LIST: "Zur Kapitelübersicht zurückkehren", PAGES_LOADED: "Seiten geladen", GO_TO_PAGE: "Gehe zu Seite", ENLARGE: "Vergrößern", RESTORE: "Wiederherstellen", REDUCE: "Wiederherstellen", FIT_WIDTH: "Breite anpassen", FIT_HEIGHT: "Höhe anpassen", PERCENT: "Prozent", TOGGLE_CONTROLS: "Seitensteuerung umschalten", ZOOM_IN: "Hineinzoomen", ZOOM_OUT: "Herauszoomen", ZOOM_RESET: "Zoom zurücksetzen", ZOOM_WIDTH: "Auf Breite zoomen", ZOOM_HEIGHT: "Auf Höhe zoomen", HIDE: "Ausblenden", RELOAD: "Neu laden", SLOWLY: "Langsam", NORMAL: "Normal", FAST: "Schnell", EXTREME: "Extrem", ALL_PAGES: "Alle Seiten", SPEED_WARNING: "Ladegeschwindigkeit zu hoch", SPEED_WARNING_MESSAGE: "Diese Geschwindigkeit wird nicht empfohlen.
Sie kann einige Server überlasten oder deine IP als DDoS-Angreifer markieren.
Bitte mit Vorsicht verwenden!", SCROLL_UP: "Nach oben scrollen", SCROLL_DOWN: "Nach unten scrollen", CLOSE: "Schließen", CANCEL: "Abbrechen", LIST_EMPTY: "Liste leer", SCROLL_START: "Auto-Scroll umschalten", INCREASE_SPEED: "Scrollgeschwindigkeit erhöhen", DECREASE_SPEED: "Scrollgeschwindigkeit verringern", AUTO_SCROLL_HEIGHT: "Auto-Scroll-Geschwindigkeit in Pixel", VERTICAL_SEPARATOR: "Vertikale Trenner anzeigen", END: "Ende", SCOPE: "Bereich", GLOBAL: "Global", GENERAL: "Allgemein", LOADING: "Lädt", ZOOM: "Zoom", OTHERS: "Sonstiges", NAVBAR_TYPE: "Navigationsleistentyp ändern", NAVBAR_BOTTOM: "Unten", NAVBAR_LEFT: "Links", NAVBAR_RIGHT: "Rechts", NAVBAR_DISABLED: "Deaktiviert", PAGINATION_TYPE: "Paginierungstyp", PAGINATION_DISABLED: "Deaktiviert", PAGINATION_SLIDER: "Schieberegler", PAGINATION_ARROWS: "Seitenpfeile", PAGINATION_BOTH: "Beides", FILE_MENU: "Hauptmenü", VIEW_MENU: "Menü „Ansicht“", ZOOM_MENU: "Zoom-Menü", DOUBLE_PAGE: "Doppelseite umschalten", CHOOSE_FILE: "Datei auswählen", NO_FILES_SELECTED: "Keine Dateien ausgewählt" }, { ID: "fr_FR", NAME: "Français (FR)", STARTING: "Démarrage Manga OnlineViewer", RESUME: "Reprise de la lecture à partir de la Page ", WAITING: "Veuillez patienter, 3 secondes...", CHOOSE_BEGINNING: "Choisissez la page par laquelle commencer :", BUTTON_START: "Démarrer Manga OnlineViewer", SETTINGS: "Paramètres", LANGUAGE: "Langue", COLOR_SCHEME: "Palette de couleurs", THEME: "Thème", THEME_COLOR: "Couleur", THEME_HUE: "Teinte de couleur", THEME_SHADE: "Nuance de couleur", DEFAULT_LOAD_MODE: "Mode de chargement par défaut", LOAD_MODE_NORMAL: "Normal (attendre 3 s)", LOAD_MODE_ALWAYS: "Toujours (immédiatement)", LOAD_MODE_NEVER: "Jamais (manuellement)", LOAD_SPEED: "Vitesse de chargement", DEFAULT_ZOOM: "Zoom par défaut (entre 5 et 200)", DEFAULT_ZOOM_MODE: "Mode de zoom par défaut", MINIMUM_ZOOM: "Zoom minimum par rapport à la largeur de l'écran (entre 30 et 100)", ZOOM_STEP: "Pas de changement de zoom (entre 5 et 50)", DEFAULT_VIEW_MODE: "Mode d'affichage par défaut", VIEW_MODE_VERTICAL: "Vertical", VIEW_MODE_LEFT: "Horizontal - De gauche à droite", VIEW_MODE_RIGHT: "Horizontal - De droite à gauche", VIEW_MODE_WEBCOMIC: "WebComic", VIEW_MODE_BOOK: "Livre - De gauche à droite", VIEW_MODE_MANGA: "Manga - De droite à gauche", VIEW_MODE_GALLERY: "Galerie", FIT_WIDTH_OVERSIZED: "Ajuster à la largeur si surdimensionné", SHOW_THUMBNAILS: "Afficher les vignettes", HIDE_CONTROLS: "Toujours masquer les contrôles de page", HEADER_TYPE: "Changer le type d'en-tête", HEADER_HOVER: "Survol", HEADER_SCROLL: "Défilement", HEADER_CLICK: "Clic", HEADER_FIXED: "Fixe", HEADER_SIMPLE: "Simple", BUTTON_DOWNLOAD: "Télécharger", DOWNLOAD_ZIP: "Télécharger le fichier Zip", DOWNLOAD_IMAGES: "Télécharger les images en Zip automatiquement", DOWNLOAD_PROGRESS: "Téléchargement : ##num## sur ##total##", GENERATING_ZIP: "Génération du fichier Zip...", DOWNLOAD_INCOMPLETE: "Téléchargement incomplet", DOWNLOAD_INCOMPLETE_MESSAGE: "Certaines pages n'ont pas pu être téléchargées et ont été ignorées. Une liste des pages concernées a été ajoutée au fichier ZIP.", BUTTON_NEXT: "Suivant", NEXT_CHAPTER: "Chapitre suivant", BUTTON_PREVIOUS: "Précédent", PREVIOUS_CHAPTER: "Chapitre précédent", BOOKMARKS: "Favoris", BOOKMARK: "Favori", BOOKMARK_REMOVED: "Favori supprimé", BOOKMARK_SAVED: "Favori enregistré", BOOKMARK_MESSAGE: "La prochaine fois que vous ouvrirez ce chapitre, il reprendra à partir de: Page ##num## (Seulement UNE FOIS par favori)", KEYBINDINGS: "Raccourcis clavier", EDIT_KEYBINDS: "Modifier les raccourcis clavier", SAVE_KEYBINDS: "Enregistrer les raccourcis clavier", BUTTON_EDIT: "Modifier", BUTTON_SAVE: "Enregistrer", KEYBIND_RULES: `

Touches prises en charge

Modificateurs autorisés : shift, option, alt, ctrl, control, command.
Touches spéciales : backspace, tab, clear, enter, return, esc, escape, space, up, down, left, right, home, end, pageup, pagedown, del, delete, f1 - f19, num_0 - num_9, num_multiply, num_add, num_enter, num_subtract, num_decimal, num_divide.
Exemples : a, ctrl+a , shift+a , num_2 , 2 `, ATTENTION: "Attention", WARNING: "Avertissement", BUTTON_RESET_SETTINGS: "Réinitialiser les paramètres", SETTINGS_RESET: "Les paramètres ont été réinitialisés, rechargez la page pour prendre effet", LANGUAGE_CHANGED: "La langue a été modifiée, rechargez la page pour prendre effet", AUTO_DOWNLOAD: "La prochaine fois qu'un chapitre finira de se charger, il vous sera proposé de l'enregistrer automatiquement", LAZY_LOAD: "Le chargement paresseux est incompatible avec le téléchargement zip, vous ne pourrez pas télécharger avec ce paramètre activé.
Suggestion : Désactivez les vignettes pour économiser de la bande passante/mémoire.", LAZY_LOAD_IMAGES_ENABLE: "Activer le chargement paresseux des images", LAZY_LOAD_IMAGES: "Début du chargement paresseux à partir de la page (entre 5 et 100)", RETURN_CHAPTER_LIST: "Retour à la liste des chapitres", PAGES_LOADED: "Pages chargées", GO_TO_PAGE: "Aller à la page", ENLARGE: "Agrandir", RESTORE: "Restaurer", REDUCE: "Réduire", FIT_WIDTH: "Ajuster à la largeur", FIT_HEIGHT: "Ajuster à la hauteur", PERCENT: "Pourcentage", TOGGLE_CONTROLS: "Basculer les contrôles de page", ZOOM_IN: "Zoom avant", ZOOM_OUT: "Zoom arrière", ZOOM_RESET: "Réinitialiser le zoom", ZOOM_WIDTH: "Zoomer à la largeur", ZOOM_HEIGHT: "Zoomer à la hauteur", HIDE: "Masquer", RELOAD: "Recharger", SLOWLY: "Lentement", NORMAL: "Normal", FAST: "Rapide", EXTREME: "Extrême", ALL_PAGES: "Toutes les pages", SPEED_WARNING: "Vitesse de chargement trop élevée", SPEED_WARNING_MESSAGE: "Cette vitesse n'est pas recommandée.
Elle peut nuire à certains serveurs ou marquer votre IP comme un attaquant DDoS.
Veuillez l'utiliser avec prudence !", SCROLL_UP: "Faire défiler vers le haut", SCROLL_DOWN: "Faire défiler vers le bas", CLOSE: "Fermer", CANCEL: "Annuler", LIST_EMPTY: "Liste vide", SCROLL_START: "Basculer le défilement automatique", INCREASE_SPEED: "Augmenter la vitesse de défilement", DECREASE_SPEED: "Diminuer la vitesse de défilement", AUTO_SCROLL_HEIGHT: "Vitesse de défilement automatique en pixels", VERTICAL_SEPARATOR: "Afficher les séparateurs verticaux", END: "Fin", SCOPE: "Portée", GLOBAL: "Global", GENERAL: "Général", LOADING: "Chargement", ZOOM: "Zoom", OTHERS: "Autres", NAVBAR_TYPE: "Changer le type de barre de navigation", NAVBAR_BOTTOM: "Bas", NAVBAR_LEFT: "Gauche", NAVBAR_RIGHT: "Droite", NAVBAR_DISABLED: "Désactivé", PAGINATION_TYPE: "Type de pagination", PAGINATION_DISABLED: "Désactivé", PAGINATION_SLIDER: "Curseur", PAGINATION_ARROWS: "Flèches latérales", PAGINATION_BOTH: "Les deux", FILE_MENU: "Menu principal", VIEW_MENU: "Menu Affichage", ZOOM_MENU: "Menu Zoom", DOUBLE_PAGE: "Basculer Double Page", CHOOSE_FILE: "Choisir un fichier", NO_FILES_SELECTED: "Aucun fichier sélectionné" } ]; //#endregion //#region src/types/IManga.ts /** * A type guard to check if a manga object uses the direct image list strategy. * @param {IManga} manga - The manga object to check. * @returns {manga is IMangaImages} `true` if the manga object has a `listImages` property. */ function isImagesManga(manga) { return "listImages" in manga && !isNothing(manga.listImages); } /** * A type guard to check if a manga object uses the page scraping strategy. * @param {IManga} manga - The manga object to check. * @returns {manga is IMangaPages} `true` if the manga object has a `listPages` property. */ function isPagesManga(manga) { return "listPages" in manga && !isNothing(manga.listPages); } /** * A type guard to check if a manga object uses the brute-force strategy. * @param {IManga} manga - The manga object to check. * @returns {manga is IMangaForce} `true` if the manga object has a `bruteForce` property. */ function isBruteforceManga(manga) { return "bruteForce" in manga && !isNothing(manga.bruteForce); } //#endregion //#region src/types/ISite.ts /** * Enumeration of supported content languages. */ var Language = /* @__PURE__ */ function(Language) { Language["ENGLISH"] = "English"; Language["SPANISH"] = "Spanish"; Language["PORTUGUESE"] = "Portuguese"; Language["CHINESE"] = "Chinese"; Language["RAW"] = "Raw"; return Language; }({}); /** * Enumeration of supported content categories. */ var Category = /* @__PURE__ */ function(Category) { Category["MANGA"] = "manga"; Category["COMIC"] = "comic"; Category["HENTAI"] = "hentai"; return Category; }({}); //#endregion //#region src/types/index.ts /** * A type guard to check if a given key exists on an object in a type-safe way. * This allows TypeScript to narrow the type of the key within conditional blocks. * @template T - The type of the object. * @param {T} obj - The object to check. * @param {PropertyKey} key - The key to check for. * @returns {key is keyof T} `true` if the key is a valid key of the object, `false` otherwise. */ function isKey(obj, key) { return key in obj; } //#endregion //#region node_modules/@lit/reactive-element/css-tag.js /** * @license * Copyright 2019 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var t$2 = globalThis; var e$5 = t$2.ShadowRoot && (void 0 === t$2.ShadyCSS || t$2.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype; var s$1 = Symbol(); var o$9 = /* @__PURE__ */ new WeakMap(); var n$3 = class { constructor(t, e, o) { if (this._$cssResult$ = !0, o !== s$1) throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead."); this.cssText = t, this.t = e; } get styleSheet() { let t = this.o; const s = this.t; if (e$5 && void 0 === t) { const e = void 0 !== s && 1 === s.length; e && (t = o$9.get(s)), void 0 === t && ((this.o = t = new CSSStyleSheet()).replaceSync(this.cssText), e && o$9.set(s, t)); } return t; } toString() { return this.cssText; } }; var r$4 = (t) => new n$3("string" == typeof t ? t : t + "", void 0, s$1); var i$3 = (t, ...e) => { return new n$3(1 === t.length ? t[0] : e.reduce((e, s, o) => e + ((t) => { if (!0 === t._$cssResult$) return t.cssText; if ("number" == typeof t) return t; throw Error("Value passed to 'css' function must be a 'css' function result: " + t + ". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security."); })(s) + t[o + 1], t[0]), t, s$1); }; var S = (s, o) => { if (e$5) s.adoptedStyleSheets = o.map((t) => t instanceof CSSStyleSheet ? t : t.styleSheet); else for (const e of o) { const o = document.createElement("style"), n = t$2.litNonce; void 0 !== n && o.setAttribute("nonce", n), o.textContent = e.cssText, s.appendChild(o); } }; var c$1 = e$5 ? (t) => t : (t) => t instanceof CSSStyleSheet ? ((t) => { let e = ""; for (const s of t.cssRules) e += s.cssText; return r$4(e); })(t) : t; //#endregion //#region node_modules/@lit/reactive-element/reactive-element.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var { is: i$2, defineProperty: e$4, getOwnPropertyDescriptor: h, getOwnPropertyNames: r$3, getOwnPropertySymbols: o$8, getPrototypeOf: n$2 } = Object, a = globalThis, c = a.trustedTypes, l = c ? c.emptyScript : "", p = a.reactiveElementPolyfillSupport, d$1 = (t, s) => t, u = { toAttribute(t, s) { switch (s) { case Boolean: t = t ? l : null; break; case Object: case Array: t = null == t ? t : JSON.stringify(t); } return t; }, fromAttribute(t, s) { let i = t; switch (s) { case Boolean: i = null !== t; break; case Number: i = null === t ? null : Number(t); break; case Object: case Array: try { i = JSON.parse(t); } catch (t) { i = null; } } return i; } }, f = (t, s) => !i$2(t, s), b = { attribute: !0, type: String, converter: u, reflect: !1, useDefault: !1, hasChanged: f }; Symbol.metadata ??= Symbol("metadata"), a.litPropertyMetadata ??= /* @__PURE__ */ new WeakMap(); var y = class extends HTMLElement { static addInitializer(t) { this._$Ei(), (this.l ??= []).push(t); } static get observedAttributes() { return this.finalize(), this._$Eh && [...this._$Eh.keys()]; } static createProperty(t, s = b) { if (s.state && (s.attribute = !1), this._$Ei(), this.prototype.hasOwnProperty(t) && ((s = Object.create(s)).wrapped = !0), this.elementProperties.set(t, s), !s.noAccessor) { const i = Symbol(), h = this.getPropertyDescriptor(t, i, s); void 0 !== h && e$4(this.prototype, t, h); } } static getPropertyDescriptor(t, s, i) { const { get: e, set: r } = h(this.prototype, t) ?? { get() { return this[s]; }, set(t) { this[s] = t; } }; return { get: e, set(s) { const h = e?.call(this); r?.call(this, s), this.requestUpdate(t, h, i); }, configurable: !0, enumerable: !0 }; } static getPropertyOptions(t) { return this.elementProperties.get(t) ?? b; } static _$Ei() { if (this.hasOwnProperty(d$1("elementProperties"))) return; const t = n$2(this); t.finalize(), void 0 !== t.l && (this.l = [...t.l]), this.elementProperties = new Map(t.elementProperties); } static finalize() { if (this.hasOwnProperty(d$1("finalized"))) return; if (this.finalized = !0, this._$Ei(), this.hasOwnProperty(d$1("properties"))) { const t = this.properties, s = [...r$3(t), ...o$8(t)]; for (const i of s) this.createProperty(i, t[i]); } const t = this[Symbol.metadata]; if (null !== t) { const s = litPropertyMetadata.get(t); if (void 0 !== s) for (const [t, i] of s) this.elementProperties.set(t, i); } this._$Eh = /* @__PURE__ */ new Map(); for (const [t, s] of this.elementProperties) { const i = this._$Eu(t, s); void 0 !== i && this._$Eh.set(i, t); } this.elementStyles = this.finalizeStyles(this.styles); } static finalizeStyles(s) { const i = []; if (Array.isArray(s)) { const e = new Set(s.flat(Infinity).reverse()); for (const s of e) i.unshift(c$1(s)); } else void 0 !== s && i.push(c$1(s)); return i; } static _$Eu(t, s) { const i = s.attribute; return !1 === i ? void 0 : "string" == typeof i ? i : "string" == typeof t ? t.toLowerCase() : void 0; } constructor() { super(), this._$Ep = void 0, this.isUpdatePending = !1, this.hasUpdated = !1, this._$Em = null, this._$Ev(); } _$Ev() { this._$ES = new Promise((t) => this.enableUpdating = t), this._$AL = /* @__PURE__ */ new Map(), this._$E_(), this.requestUpdate(), this.constructor.l?.forEach((t) => t(this)); } addController(t) { (this._$EO ??= /* @__PURE__ */ new Set()).add(t), void 0 !== this.renderRoot && this.isConnected && t.hostConnected?.(); } removeController(t) { this._$EO?.delete(t); } _$E_() { const t = /* @__PURE__ */ new Map(), s = this.constructor.elementProperties; for (const i of s.keys()) this.hasOwnProperty(i) && (t.set(i, this[i]), delete this[i]); t.size > 0 && (this._$Ep = t); } createRenderRoot() { const t = this.shadowRoot ?? this.attachShadow(this.constructor.shadowRootOptions); return S(t, this.constructor.elementStyles), t; } connectedCallback() { this.renderRoot ??= this.createRenderRoot(), this.enableUpdating(!0), this._$EO?.forEach((t) => t.hostConnected?.()); } enableUpdating(t) {} disconnectedCallback() { this._$EO?.forEach((t) => t.hostDisconnected?.()); } attributeChangedCallback(t, s, i) { this._$AK(t, i); } _$ET(t, s) { const i = this.constructor.elementProperties.get(t), e = this.constructor._$Eu(t, i); if (void 0 !== e && !0 === i.reflect) { const h = (void 0 !== i.converter?.toAttribute ? i.converter : u).toAttribute(s, i.type); this._$Em = t, null == h ? this.removeAttribute(e) : this.setAttribute(e, h), this._$Em = null; } } _$AK(t, s) { const i = this.constructor, e = i._$Eh.get(t); if (void 0 !== e && this._$Em !== e) { const t = i.getPropertyOptions(e), h = "function" == typeof t.converter ? { fromAttribute: t.converter } : void 0 !== t.converter?.fromAttribute ? t.converter : u; this._$Em = e; const r = h.fromAttribute(s, t.type); this[e] = r ?? this._$Ej?.get(e) ?? r, this._$Em = null; } } requestUpdate(t, s, i, e = !1, h) { if (void 0 !== t) { const r = this.constructor; if (!1 === e && (h = this[t]), i ??= r.getPropertyOptions(t), !((i.hasChanged ?? f)(h, s) || i.useDefault && i.reflect && h === this._$Ej?.get(t) && !this.hasAttribute(r._$Eu(t, i)))) return; this.C(t, s, i); } !1 === this.isUpdatePending && (this._$ES = this._$EP()); } C(t, s, { useDefault: i, reflect: e, wrapped: h }, r) { i && !(this._$Ej ??= /* @__PURE__ */ new Map()).has(t) && (this._$Ej.set(t, r ?? s ?? this[t]), !0 !== h || void 0 !== r) || (this._$AL.has(t) || (this.hasUpdated || i || (s = void 0), this._$AL.set(t, s)), !0 === e && this._$Em !== t && (this._$Eq ??= /* @__PURE__ */ new Set()).add(t)); } async _$EP() { this.isUpdatePending = !0; try { await this._$ES; } catch (t) { Promise.reject(t); } const t = this.scheduleUpdate(); return null != t && await t, !this.isUpdatePending; } scheduleUpdate() { return this.performUpdate(); } performUpdate() { if (!this.isUpdatePending) return; if (!this.hasUpdated) { if (this.renderRoot ??= this.createRenderRoot(), this._$Ep) { for (const [t, s] of this._$Ep) this[t] = s; this._$Ep = void 0; } const t = this.constructor.elementProperties; if (t.size > 0) for (const [s, i] of t) { const { wrapped: t } = i, e = this[s]; !0 !== t || this._$AL.has(s) || void 0 === e || this.C(s, void 0, i, e); } } let t = !1; const s = this._$AL; try { t = this.shouldUpdate(s), t ? (this.willUpdate(s), this._$EO?.forEach((t) => t.hostUpdate?.()), this.update(s)) : this._$EM(); } catch (s) { throw t = !1, this._$EM(), s; } t && this._$AE(s); } willUpdate(t) {} _$AE(t) { this._$EO?.forEach((t) => t.hostUpdated?.()), this.hasUpdated || (this.hasUpdated = !0, this.firstUpdated(t)), this.updated(t); } _$EM() { this._$AL = /* @__PURE__ */ new Map(), this.isUpdatePending = !1; } get updateComplete() { return this.getUpdateComplete(); } getUpdateComplete() { return this._$ES; } shouldUpdate(t) { return !0; } update(t) { this._$Eq &&= this._$Eq.forEach((t) => this._$ET(t, this[t])), this._$EM(); } updated(t) {} firstUpdated(t) {} }; y.elementStyles = [], y.shadowRootOptions = { mode: "open" }, y[d$1("elementProperties")] = /* @__PURE__ */ new Map(), y[d$1("finalized")] = /* @__PURE__ */ new Map(), p?.({ ReactiveElement: y }), (a.reactiveElementVersions ??= []).push("2.1.2"); //#endregion //#region node_modules/lit-element/lit-element.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var s = globalThis; var i$1 = class extends y { constructor() { super(...arguments), this.renderOptions = { host: this }, this._$Do = void 0; } createRenderRoot() { const t = super.createRenderRoot(); return this.renderOptions.renderBefore ??= t.firstChild, t; } update(t) { const r = this.render(); this.hasUpdated || (this.renderOptions.isConnected = this.isConnected), super.update(t), this._$Do = D(r, this.renderRoot, this.renderOptions); } connectedCallback() { super.connectedCallback(), this._$Do?.setConnected(!0); } disconnectedCallback() { super.disconnectedCallback(), this._$Do?.setConnected(!1); } render() { return E; } }; i$1._$litElement$ = !0, i$1["finalized"] = !0, s.litElementHydrateSupport?.({ LitElement: i$1 }); var o$7 = s.litElementPolyfillSupport; o$7?.({ LitElement: i$1 }); (s.litElementVersions ??= []).push("4.2.2"); //#endregion //#region node_modules/@lit/reactive-element/decorators/custom-element.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var t$1 = (t) => (e, o) => { void 0 !== o ? o.addInitializer(() => { customElements.define(t, e); }) : customElements.define(t, e); }; //#endregion //#region node_modules/@lit/reactive-element/decorators/property.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var o$6 = { attribute: !0, type: String, converter: u, reflect: !1, hasChanged: f }; var r$2 = (t = o$6, e, r) => { const { kind: n, metadata: i } = r; let s = globalThis.litPropertyMetadata.get(i); if (void 0 === s && globalThis.litPropertyMetadata.set(i, s = /* @__PURE__ */ new Map()), "setter" === n && ((t = Object.create(t)).wrapped = !0), s.set(r.name, t), "accessor" === n) { const { name: o } = r; return { set(r) { const n = e.get.call(this); e.set.call(this, r), this.requestUpdate(o, n, t, !0, r); }, init(e) { return void 0 !== e && this.C(o, void 0, t, e), e; } }; } if ("setter" === n) { const { name: o } = r; return function(r) { const n = this[o]; e.call(this, r), this.requestUpdate(o, n, t, !0, r); }; } throw Error("Unsupported decorator location: " + n); }; function n$1(t) { return (e, o) => "object" == typeof o ? r$2(t, e, o) : ((t, e, o) => { const r = e.hasOwnProperty(o); return e.constructor.createProperty(o, t), r ? Object.getOwnPropertyDescriptor(e, o) : void 0; })(t, e, o); } //#endregion //#region node_modules/@lit/reactive-element/decorators/state.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ function r$1(r) { return n$1({ ...r, state: !0, attribute: !1 }); } //#endregion //#region node_modules/@lit/reactive-element/decorators/base.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var e$3 = (e, t, c) => (c.configurable = !0, c.enumerable = !0, Reflect.decorate && "object" != typeof t && Object.defineProperty(e, t, c), c); //#endregion //#region node_modules/@lit/reactive-element/decorators/query.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ function e$2(e, r) { return (n, s, i) => { const o = (t) => t.renderRoot?.querySelector(e) ?? null; if (r) { const { get: e, set: r } = "object" == typeof s ? n : i ?? (() => { const t = Symbol(); return { get() { return this[t]; }, set(e) { this[t] = e; } }; })(); return e$3(n, s, { get() { let t = e.call(this); return void 0 === t && (t = o(this), (null !== t || this.hasUpdated) && r.call(this, t)), t; } }); } return e$3(n, s, { get() { return o(this); } }); }; } //#endregion //#region node_modules/lit-html/directives/class-map.js /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var e$1 = e$7(class extends i$4 { constructor(t) { if (super(t), t.type !== t$3.ATTRIBUTE || "class" !== t.name || t.strings?.length > 2) throw Error("`classMap()` can only be used in the `class` attribute and must be the only part in the attribute."); } render(t) { return " " + Object.keys(t).filter((s) => t[s]).join(" ") + " "; } update(s, [i]) { if (void 0 === this.st) { this.st = /* @__PURE__ */ new Set(), void 0 !== s.strings && (this.nt = new Set(s.strings.join(" ").split(/\s/).filter((t) => "" !== t))); for (const t in i) i[t] && !this.nt?.has(t) && this.st.add(t); return this.render(i); } const r = s.element.classList; for (const t of this.st) t in i || (r.remove(t), this.st.delete(t)); for (const t in i) { const s = !!i[t]; s === this.st.has(t) || this.nt?.has(t) || (s ? (r.add(t), this.st.add(t)) : (r.remove(t), this.st.delete(t))); } return E; } }); //#endregion //#region node_modules/lit-html/directives/unsafe-html.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var e = class extends i$4 { constructor(i) { if (super(i), this.it = A, i.type !== t$3.CHILD) throw Error(this.constructor.directiveName + "() can only be used in child bindings"); } render(r) { if (r === A || null == r) return this._t = void 0, this.it = r; if (r === E) return r; if ("string" != typeof r) throw Error(this.constructor.directiveName + "() called with a non-string value"); if (r === this.it) return this._t; this.it = r; const s = [r]; return s.raw = s, this._t = { _$litType$: this.constructor.resultType, strings: s, values: [] }; } }; e.directiveName = "unsafeHTML", e.resultType = 1; var o$5 = e$7(e); //#endregion //#region node_modules/lit-html/directives/unsafe-svg.js /** * @license * Copyright 2017 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var t = class extends e {}; t.directiveName = "unsafeSVG", t.resultType = 2; var o$4 = e$7(t); //#endregion //#region src/utils/format.ts /** * Converts a string from kebab-case or snake_case to PascalCase, ensuring it starts with "Icon". * If the input already appears to be in the correct format, it is returned as is. * @param {string} name - The input string to convert. * @example * toPascalCase('align-box-right-top') // "IconAlignBoxRightTop" * toPascalCase('IconUserPlus') // "IconUserPlus" * @returns {string} The name in PascalCase with the 'Icon' prefix (e.g., "IconAlignBoxRightTopFilled"). */ function toPascalCase(name) { if (name.startsWith("Icon") && !name.includes("-") && !name.includes("_")) return name; const withoutPrefix = name.startsWith("Icon") ? name.substring(4) : name; return `Icon${_.upperFirst(_.camelCase(withoutPrefix))}`; } //#endregion //#region src/ui/styles/icons.css?inline var icons_default = ".icon-tabler-file-download > :nth-child(n + 4) {\r\n color: gold;\r\n}\r\n\r\n.icon-tabler-arrow-autofit-width > :nth-child(n + 3) {\r\n color: yellow;\r\n}\r\n\r\n.icon-tabler-arrow-autofit-height > :nth-child(n + 3) {\r\n color: yellow;\r\n}\r\n\r\n.icon-tabler-zoom-in-area > :nth-child(2),\r\n.icon-tabler-zoom-in-area > :nth-child(3) {\r\n color: lime;\r\n}\r\n\r\n.icon-tabler-zoom-out-area > :nth-child(2) {\r\n color: red;\r\n}\r\n\r\n.icon-tabler-zoom-pan > :nth-child(n + 4) {\r\n color: #9966ff;\r\n}\r\n\r\n.icon-tabler-arrow-autofit-down > :nth-child(n + 3) {\r\n color: #28ffbf;\r\n}\r\n\r\n.icon-tabler-arrow-autofit-left > :nth-child(n + 3) {\r\n color: #28ffbf;\r\n}\r\n\r\n.icon-tabler-arrow-autofit-right > :nth-child(n + 3) {\r\n color: #28ffbf;\r\n}\r\n\r\n.icon-tabler-spacing-vertical > :nth-child(4) {\r\n color: fuchsia;\r\n}\r\n\r\n.icon-tabler-spacing-horizontal > :nth-child(4) {\r\n color: fuchsia;\r\n}\r\n\r\n.icon-tabler-list-numbers > :nth-child(n + 5) {\r\n color: #e48900;\r\n}\r\n\r\n.icon-tabler-bookmarks > :nth-child(n + 2) {\r\n color: orange;\r\n}\r\n\r\n.icon-tabler-bookmark > :nth-child(2) {\r\n color: orange;\r\n}\r\n\r\n.icon-tabler-bookmark-off > :nth-child(2) {\r\n color: orange;\r\n}\r\n\r\n.icon-tabler-bookmark-off > :nth-child(3) {\r\n color: red;\r\n}\r\n\r\n.icon-tabler-eye-off > :nth-child(4) {\r\n color: red;\r\n}\r\n\r\n.icon-tabler-zoom-cancel > :nth-child(3),\r\n.icon-tabler-zoom-cancel > :nth-child(4) {\r\n color: #9966ff;\r\n}\r\n\r\n.icon-tabler-zoom-in > :nth-child(3),\r\n.icon-tabler-zoom-in > :nth-child(4) {\r\n color: lime;\r\n}\r\n\r\n.icon-tabler-zoom-out > :nth-child(3) {\r\n color: red;\r\n}\r\n\r\n.icon-tabler-refresh > :nth-child(n + 2) {\r\n color: cyan;\r\n}\r\n\r\n.icon-tabler-photo > :nth-child(n + 2) {\r\n color: silver;\r\n}\r\n\r\n.icon-tabler-photo-off > :nth-child(n + 2) {\r\n color: silver;\r\n}\r\n\r\n.icon-tabler-photo-off > :nth-child(6) {\r\n color: orange;\r\n}\r\n\r\n.icon-tabler-message > :nth-child(2),\r\n.icon-tabler-message > :nth-child(3) {\r\n color: greenyellow;\r\n}\r\n\r\n.icon-tabler-book-arrow-left > :nth-child(7),\r\n.icon-tabler-book-arrow-left > :nth-child(8),\r\n.icon-tabler-book-arrow-right > :nth-child(7),\r\n.icon-tabler-book-arrow-right > :nth-child(8),\r\n.icon-tabler-books-return > :nth-child(8),\r\n.icon-tabler-books-return > :nth-child(9) {\r\n color: greenyellow;\r\n}\r\n\r\n.icon-tabler-file-percent > :nth-child(2),\r\n.icon-tabler-file-percent > :nth-child(5),\r\n.icon-tabler-file-percent > :nth-child(6) {\r\n color: yellow;\r\n}\r\n\r\n.icon-tabler-settings-off > :nth-child(4) {\r\n color: red;\r\n}\r\n\r\n.icon-tabler-book-off > :nth-child(7) {\r\n color: red;\r\n}\r\n"; //#endregion //#region src/ui/icons/adjustments-horizontal.svg var adjustments_horizontal_default = ""; //#endregion //#region src/ui/icons/alert-circle.svg var alert_circle_default = ""; //#endregion //#region src/ui/icons/api-book.svg var api_book_default = ""; //#endregion //#region src/ui/icons/arrow-autofit-down.svg var arrow_autofit_down_default = ""; //#endregion //#region src/ui/icons/arrow-autofit-height.svg var arrow_autofit_height_default = ""; //#endregion //#region src/ui/icons/arrow-autofit-left.svg var arrow_autofit_left_default = ""; //#endregion //#region src/ui/icons/arrow-autofit-right.svg var arrow_autofit_right_default = ""; //#endregion //#region src/ui/icons/arrow-autofit-width.svg var arrow_autofit_width_default = ""; //#endregion //#region src/ui/icons/arrow-big-left.svg var arrow_big_left_default = ""; //#endregion //#region src/ui/icons/arrow-big-right.svg var arrow_big_right_default = ""; //#endregion //#region src/ui/icons/arrows-horizontal.svg var arrows_horizontal_default = ""; //#endregion //#region src/ui/icons/arrows-left-right.svg var arrows_left_right_default = ""; //#endregion //#region src/ui/icons/arrows-move.svg var arrows_move_default = ""; //#endregion //#region src/ui/icons/arrows-move-vertical.svg var arrows_move_vertical_default = ""; //#endregion //#region src/ui/icons/arrows-vertical.svg var arrows_vertical_default = ""; //#endregion //#region src/ui/icons/book.svg var book_default$1 = ""; //#endregion //#region src/ui/icons/book-arrow-left.svg var book_arrow_left_default = ""; //#endregion //#region src/ui/icons/book-arrow-right.svg var book_arrow_right_default = ""; //#endregion //#region src/ui/icons/book-off.svg var book_off_default = ""; //#endregion //#region src/ui/icons/book-upload.svg var book_upload_default = ""; //#endregion //#region src/ui/icons/bookmark.svg var bookmark_default = ""; //#endregion //#region src/ui/icons/bookmark-off.svg var bookmark_off_default = ""; //#endregion //#region src/ui/icons/bookmarks.svg var bookmarks_default$1 = ""; //#endregion //#region src/ui/icons/books-return.svg var books_return_default = ""; //#endregion //#region src/ui/icons/box-align-top.svg var box_align_top_default = ""; //#endregion //#region src/ui/icons/Comic1-SpecialFlat.svg var Comic1_SpecialFlat_default = ""; //#endregion //#region src/ui/icons/Comic1-SpecialLinealColor.svg var Comic1_SpecialLinealColor_default = ""; //#endregion //#region src/ui/icons/Comic2-SpecialFlat.svg var Comic2_SpecialFlat_default = ""; //#endregion //#region src/ui/icons/Comic2-SpecialLinealColor.svg var Comic2_SpecialLinealColor_default = ""; //#endregion //#region src/ui/icons/Comic3-SpecialFlat.svg var Comic3_SpecialFlat_default = ""; //#endregion //#region src/ui/icons/Comic3-SpecialLinealColor.svg var Comic3_SpecialLinealColor_default = ""; //#endregion //#region src/ui/icons/category.svg var category_default = ""; //#endregion //#region src/ui/icons/check.svg var check_default = ""; //#endregion //#region src/ui/icons/chevron-left.svg var chevron_left_default = ""; //#endregion //#region src/ui/icons/chevron-right.svg var chevron_right_default = ""; //#endregion //#region src/ui/icons/circle-check.svg var circle_check_default = ""; //#endregion //#region src/ui/icons/circle-x.svg var circle_x_default = ""; //#endregion //#region src/ui/icons/device-floppy.svg var device_floppy_default = ""; //#endregion //#region src/ui/icons/dots-vertical.svg var dots_vertical_default = ""; //#endregion //#region src/ui/icons/E-reader1-KawaiiFlat.svg var E_reader1_KawaiiFlat_default = ""; //#endregion //#region src/ui/icons/E-reader1-KawaiiLinealColor.svg var E_reader1_KawaiiLinealColor_default = ""; //#endregion //#region src/ui/icons/E-reader2-KawaiiFlat.svg var E_reader2_KawaiiFlat_default = ""; //#endregion //#region src/ui/icons/E-reader2-KawaiiLinealColor.svg var E_reader2_KawaiiLinealColor_default = ""; //#endregion //#region src/ui/icons/external-link.svg var external_link_default = ""; //#endregion //#region src/ui/icons/eye.svg var eye_default = ""; //#endregion //#region src/ui/icons/eye-off.svg var eye_off_default = ""; //#endregion //#region src/ui/icons/file-download.svg var file_download_default = ""; //#endregion //#region src/ui/icons/file-percent.svg var file_percent_default = ""; //#endregion //#region src/ui/icons/folder-open.svg var folder_open_default = ""; //#endregion //#region src/ui/icons/hand-click.svg var hand_click_default = ""; //#endregion //#region src/ui/icons/help.svg var help_default = ""; //#endregion //#region src/ui/icons/info-circle.svg var info_circle_default = ""; //#endregion //#region src/ui/icons/keyboard.svg var keyboard_default = ""; //#endregion //#region src/ui/icons/layout-bottombar.svg var layout_bottombar_default = ""; //#endregion //#region src/ui/icons/layout-bottombar-inactive.svg var layout_bottombar_inactive_default = ""; //#endregion //#region src/ui/icons/layout-dashboard.svg var layout_dashboard_default = ""; //#endregion //#region src/ui/icons/layout-sidebar.svg var layout_sidebar_default = ""; //#endregion //#region src/ui/icons/layout-sidebar-inactive.svg var layout_sidebar_inactive_default = ""; //#endregion //#region src/ui/icons/layout-sidebar-right.svg var layout_sidebar_right_default = ""; //#endregion //#region src/ui/icons/layout-sidebar-right-inactive.svg var layout_sidebar_right_inactive_default = ""; //#endregion //#region src/ui/icons/list-numbers.svg var list_numbers_default = ""; //#endregion //#region src/ui/icons/loader-2.svg var loader_2_default = ""; //#endregion //#region src/ui/icons/location-cog.svg var location_cog_default = ""; //#endregion //#region src/ui/icons/menu-2.svg var menu_2_default = ""; //#endregion //#region src/ui/icons/menu-deep.svg var menu_deep_default = ""; //#endregion //#region src/ui/icons/message.svg var message_default = ""; //#endregion //#region src/ui/icons/moon.svg var moon_default = ""; //#endregion //#region src/ui/icons/Page-KawaiiFlat.svg var Page_KawaiiFlat_default = ""; //#endregion //#region src/ui/icons/Page-KawaiiLinealColor.svg var Page_KawaiiLinealColor_default = ""; //#endregion //#region src/ui/icons/palette.svg var palette_default = ""; //#endregion //#region src/ui/icons/pencil.svg var pencil_default = ""; //#endregion //#region src/ui/icons/pencil-cog.svg var pencil_cog_default = ""; //#endregion //#region src/ui/icons/photo.svg var photo_default = ""; //#endregion //#region src/ui/icons/photo-off.svg var photo_off_default = ""; //#endregion //#region src/ui/icons/pin.svg var pin_default = ""; //#endregion //#region src/ui/icons/player-pause.svg var player_pause_default = ""; //#endregion //#region src/ui/icons/player-play.svg var player_play_default = ""; //#endregion //#region src/ui/icons/refresh.svg var refresh_default = ""; //#endregion //#region src/ui/icons/settings.svg var settings_default$1 = ""; //#endregion //#region src/ui/icons/settings-off.svg var settings_off_default = ""; //#endregion //#region src/ui/icons/spacing-horizontal.svg var spacing_horizontal_default = ""; //#endregion //#region src/ui/icons/spacing-vertical.svg var spacing_vertical_default = ""; //#endregion //#region src/ui/icons/sun.svg var sun_default = ""; //#endregion //#region src/ui/icons/trash.svg var trash_default = ""; //#endregion //#region src/ui/icons/world-cog.svg var world_cog_default = ""; //#endregion //#region src/ui/icons/x.svg var x_default = ""; //#endregion //#region src/ui/icons/zoom.svg var zoom_default = ""; //#endregion //#region src/ui/icons/zoom-cancel.svg var zoom_cancel_default = ""; //#endregion //#region src/ui/icons/zoom-in.svg var zoom_in_default = ""; //#endregion //#region src/ui/icons/zoom-in-area.svg var zoom_in_area_default = ""; //#endregion //#region src/ui/icons/zoom-out.svg var zoom_out_default = ""; //#endregion //#region src/ui/icons/zoom-out-area.svg var zoom_out_area_default = ""; //#endregion //#region src/ui/icons/zoom-pan.svg var zoom_pan_default = ""; //#endregion //#region src/ui/icons/svg.ts var svg_exports = /* @__PURE__ */ __exportAll({ IconAdjustmentsHorizontal: () => adjustments_horizontal_default, IconAlertCircle: () => alert_circle_default, IconApiBook: () => api_book_default, IconArrowAutofitDown: () => arrow_autofit_down_default, IconArrowAutofitHeight: () => arrow_autofit_height_default, IconArrowAutofitLeft: () => arrow_autofit_left_default, IconArrowAutofitRight: () => arrow_autofit_right_default, IconArrowAutofitWidth: () => arrow_autofit_width_default, IconArrowBigLeft: () => arrow_big_left_default, IconArrowBigRight: () => arrow_big_right_default, IconArrowsHorizontal: () => arrows_horizontal_default, IconArrowsLeftRight: () => arrows_left_right_default, IconArrowsMove: () => arrows_move_default, IconArrowsMoveVertical: () => arrows_move_vertical_default, IconArrowsVertical: () => arrows_vertical_default, IconBook: () => book_default$1, IconBookArrowLeft: () => book_arrow_left_default, IconBookArrowRight: () => book_arrow_right_default, IconBookOff: () => book_off_default, IconBookUpload: () => book_upload_default, IconBookmark: () => bookmark_default, IconBookmarkOff: () => bookmark_off_default, IconBookmarks: () => bookmarks_default$1, IconBooksReturn: () => books_return_default, IconBoxAlignTop: () => box_align_top_default, IconCategory: () => category_default, IconCheck: () => check_default, IconChevronLeft: () => chevron_left_default, IconChevronRight: () => chevron_right_default, IconCircleCheck: () => circle_check_default, IconCircleX: () => circle_x_default, IconComic1: () => Comic1_SpecialLinealColor_default, IconComic1Flat: () => Comic1_SpecialFlat_default, IconComic2: () => Comic2_SpecialLinealColor_default, IconComic2Flat: () => Comic2_SpecialFlat_default, IconComic3: () => Comic3_SpecialLinealColor_default, IconComic3Flat: () => Comic3_SpecialFlat_default, IconDeviceFloppy: () => device_floppy_default, IconDotsVertical: () => dots_vertical_default, IconEReader1: () => E_reader1_KawaiiLinealColor_default, IconEReader1Flat: () => E_reader1_KawaiiFlat_default, IconEReader2: () => E_reader2_KawaiiLinealColor_default, IconEReader2Flat: () => E_reader2_KawaiiFlat_default, IconExternalLink: () => external_link_default, IconEye: () => eye_default, IconEyeOff: () => eye_off_default, IconFileDownload: () => file_download_default, IconFilePercent: () => file_percent_default, IconFolderOpen: () => folder_open_default, IconHandClick: () => hand_click_default, IconHelp: () => help_default, IconInfoCircle: () => info_circle_default, IconKeyboard: () => keyboard_default, IconLayoutBottombar: () => layout_bottombar_default, IconLayoutBottombarInactive: () => layout_bottombar_inactive_default, IconLayoutDashboard: () => layout_dashboard_default, IconLayoutSidebar: () => layout_sidebar_default, IconLayoutSidebarInactive: () => layout_sidebar_inactive_default, IconLayoutSidebarRight: () => layout_sidebar_right_default, IconLayoutSidebarRightInactive: () => layout_sidebar_right_inactive_default, IconListNumbers: () => list_numbers_default, IconLoader2: () => loader_2_default, IconLocationCog: () => location_cog_default, IconMenu2: () => menu_2_default, IconMenuDeep: () => menu_deep_default, IconMessage: () => message_default, IconMoon: () => moon_default, IconPage: () => Page_KawaiiLinealColor_default, IconPageFlat: () => Page_KawaiiFlat_default, IconPalette: () => palette_default, IconPencil: () => pencil_default, IconPencilCog: () => pencil_cog_default, IconPhoto: () => photo_default, IconPhotoOff: () => photo_off_default, IconPin: () => pin_default, IconPlayerPause: () => player_pause_default, IconPlayerPlay: () => player_play_default, IconRefresh: () => refresh_default, IconSettings: () => settings_default$1, IconSettingsOff: () => settings_off_default, IconSpacingHorizontal: () => spacing_horizontal_default, IconSpacingVertical: () => spacing_vertical_default, IconSun: () => sun_default, IconTrash: () => trash_default, IconWorldCog: () => world_cog_default, IconX: () => x_default, IconZoom: () => zoom_default, IconZoomCancel: () => zoom_cancel_default, IconZoomIn: () => zoom_in_default, IconZoomInArea: () => zoom_in_area_default, IconZoomOut: () => zoom_out_default, IconZoomOutArea: () => zoom_out_area_default, IconZoomPan: () => zoom_pan_default }); //#endregion //#region src/ui/icons/StyledIcons.ts /** * @file This module is responsible for processing raw SVG icon strings. * It parses a dedicated CSS file (`Icons.css`) to extract color rules, applies these rules * directly to the SVG strings by adding `stroke` attributes, and then exports the processed icons * as raw SVG strings. */ var StyledIcons_exports = /* @__PURE__ */ __exportAll({ IconAdjustmentsHorizontal: () => IconAdjustmentsHorizontal$1, IconAlertCircle: () => IconAlertCircle$1, IconApiBook: () => IconApiBook$1, IconArrowAutofitDown: () => IconArrowAutofitDown$1, IconArrowAutofitHeight: () => IconArrowAutofitHeight$1, IconArrowAutofitLeft: () => IconArrowAutofitLeft$1, IconArrowAutofitRight: () => IconArrowAutofitRight$1, IconArrowAutofitWidth: () => IconArrowAutofitWidth$1, IconArrowBigLeft: () => IconArrowBigLeft$1, IconArrowBigRight: () => IconArrowBigRight$1, IconArrowsHorizontal: () => IconArrowsHorizontal$1, IconArrowsLeftRight: () => IconArrowsLeftRight$1, IconArrowsMove: () => IconArrowsMove$1, IconArrowsMoveVertical: () => IconArrowsMoveVertical$1, IconArrowsVertical: () => IconArrowsVertical$1, IconBook: () => IconBook$1, IconBookArrowLeft: () => IconBookArrowLeft$1, IconBookArrowRight: () => IconBookArrowRight$1, IconBookOff: () => IconBookOff$1, IconBookUpload: () => IconBookUpload$1, IconBookmark: () => IconBookmark$1, IconBookmarkOff: () => IconBookmarkOff$1, IconBookmarks: () => IconBookmarks$1, IconBooksReturn: () => IconBooksReturn$1, IconBoxAlignTop: () => IconBoxAlignTop$1, IconCategory: () => IconCategory$1, IconCheck: () => IconCheck$1, IconChevronLeft: () => IconChevronLeft$1, IconChevronRight: () => IconChevronRight$1, IconCircleCheck: () => IconCircleCheck$1, IconCircleX: () => IconCircleX$1, IconComic1: () => IconComic1$1, IconComic1Flat: () => IconComic1Flat$1, IconComic2: () => IconComic2$1, IconComic2Flat: () => IconComic2Flat$1, IconComic3: () => IconComic3$1, IconComic3Flat: () => IconComic3Flat$1, IconDeviceFloppy: () => IconDeviceFloppy$1, IconDotsVertical: () => IconDotsVertical$1, IconEReader1: () => IconEReader1$1, IconEReader1Flat: () => IconEReader1Flat$1, IconEReader2: () => IconEReader2$1, IconEReader2Flat: () => IconEReader2Flat$1, IconExternalLink: () => IconExternalLink$1, IconEye: () => IconEye$1, IconEyeOff: () => IconEyeOff$1, IconFileDownload: () => IconFileDownload$1, IconFilePercent: () => IconFilePercent$1, IconFolderOpen: () => IconFolderOpen$1, IconHandClick: () => IconHandClick$1, IconHelp: () => IconHelp$1, IconInfoCircle: () => IconInfoCircle$1, IconKeyboard: () => IconKeyboard$1, IconLayoutBottombar: () => IconLayoutBottombar$1, IconLayoutBottombarInactive: () => IconLayoutBottombarInactive$1, IconLayoutDashboard: () => IconLayoutDashboard$1, IconLayoutSidebar: () => IconLayoutSidebar$1, IconLayoutSidebarInactive: () => IconLayoutSidebarInactive$1, IconLayoutSidebarRight: () => IconLayoutSidebarRight$1, IconLayoutSidebarRightInactive: () => IconLayoutSidebarRightInactive$1, IconListNumbers: () => IconListNumbers$1, IconLoader2: () => IconLoader2$1, IconLocationCog: () => IconLocationCog$1, IconMenu2: () => IconMenu2$1, IconMenuDeep: () => IconMenuDeep$1, IconMessage: () => IconMessage$1, IconMoon: () => IconMoon$1, IconPage: () => IconPage$1, IconPageFlat: () => IconPageFlat$1, IconPalette: () => IconPalette$1, IconPencil: () => IconPencil$1, IconPencilCog: () => IconPencilCog$1, IconPhoto: () => IconPhoto$1, IconPhotoOff: () => IconPhotoOff$1, IconPin: () => IconPin$1, IconPlayerPause: () => IconPlayerPause$1, IconPlayerPlay: () => IconPlayerPlay$1, IconRefresh: () => IconRefresh$1, IconSettings: () => IconSettings$1, IconSettingsOff: () => IconSettingsOff$1, IconSpacingHorizontal: () => IconSpacingHorizontal$1, IconSpacingVertical: () => IconSpacingVertical$1, IconSun: () => IconSun$1, IconTrash: () => IconTrash$1, IconWorldCog: () => IconWorldCog$1, IconX: () => IconX$1, IconZoom: () => IconZoom$1, IconZoomCancel: () => IconZoomCancel$1, IconZoomIn: () => IconZoomIn$1, IconZoomInArea: () => IconZoomInArea$1, IconZoomOut: () => IconZoomOut$1, IconZoomOutArea: () => IconZoomOutArea$1, IconZoomPan: () => IconZoomPan$1 }); /** * Parses a CSS string to extract simple color rules. * This function is intentionally simple and tailored to the specific format of `Icons.css`. * A more robust solution would require a full CSS parser, which is not necessary for this use case. * @internal * @param {string} css - The CSS string to parse. * @returns {CssRule[]} An array of parsed CSS rules. */ function parseCss(css) { return [...css.matchAll(/([^{}]+)\s*\{([^}]+)\}/g)].map((match) => { const selectorsBlock = match[1].trim(); const properties = match[2]; const colorMatch = /color:\s*([^;]+)/.exec(properties); if (colorMatch) { const color = colorMatch[1].trim(); return { selectors: selectorsBlock.split(",").map((s) => s.trim().replace(/\s\s+/g, " ")), color }; } return null; }).filter((rule) => rule !== null); } var colorRules = parseCss(icons_default); /** * A map from icon class names to the styling rules that apply to them. * This is pre-computed to optimize the color application process. * @internal */ var rulesByClassName = /* @__PURE__ */ new Map(); for (const rule of colorRules) for (const selector of rule.selectors) { const match = selector.match(/^\s*\.([^ ]+)\s*(.*)$/); if (!match) continue; const [, className, rest] = match; let subSelector = rest.trim(); if (subSelector.startsWith(">")) subSelector = subSelector.substring(1).trim(); if (subSelector === "") subSelector = "*"; if (!rulesByClassName.has(className)) rulesByClassName.set(className, []); rulesByClassName.get(className)?.push({ subSelector, color: rule.color }); } var parser = new DOMParser(); var serializer = new XMLSerializer(); /** * Applies pre-parsed CSS color rules to a raw SVG string by injecting `stroke` attributes into its child elements. * @internal * @param {string} svgString - The original SVG icon as a string. * @param {string} className - The base class name of the icon (e.g., 'icon-tabler-file-download') used for matching selectors. * @returns {string} A new SVG string with the color styles applied. */ function applyColorsToSvg(svgString, className) { const rulesForIcon = rulesByClassName.get(className); if (!rulesForIcon?.length) return svgString; const svg = parser.parseFromString(svgString, "image/svg+xml").documentElement; if (svg.querySelector("parsererror")) { console.error(`Error parsing SVG for ${className}`); return svgString; } for (const { subSelector, color } of rulesForIcon) try { svg.querySelectorAll(subSelector).forEach((el) => { el.setAttribute("stroke", color); }); } catch (e) { console.error(`Invalid selector "${subSelector}" for ${className}`, e); } return serializer.serializeToString(svg); } /** * A collection of all processed icons, exported as raw SVG strings. * Use these when you need the SVG content directly, for example, in a CMS. * @example svgToUrl(IconFileDownload) */ var { IconAdjustmentsHorizontal: IconAdjustmentsHorizontal$1, IconApiBook: IconApiBook$1, IconArrowsHorizontal: IconArrowsHorizontal$1, IconArrowsLeftRight: IconArrowsLeftRight$1, IconArrowAutofitDown: IconArrowAutofitDown$1, IconArrowAutofitHeight: IconArrowAutofitHeight$1, IconArrowAutofitLeft: IconArrowAutofitLeft$1, IconArrowAutofitRight: IconArrowAutofitRight$1, IconArrowAutofitWidth: IconArrowAutofitWidth$1, IconArrowBigLeft: IconArrowBigLeft$1, IconArrowBigRight: IconArrowBigRight$1, IconArrowsMove: IconArrowsMove$1, IconArrowsMoveVertical: IconArrowsMoveVertical$1, IconArrowsVertical: IconArrowsVertical$1, IconBook: IconBook$1, IconBookOff: IconBookOff$1, IconBookArrowLeft: IconBookArrowLeft$1, IconBookArrowRight: IconBookArrowRight$1, IconBooksReturn: IconBooksReturn$1, IconBookUpload: IconBookUpload$1, IconBookmark: IconBookmark$1, IconBookmarkOff: IconBookmarkOff$1, IconBookmarks: IconBookmarks$1, IconBoxAlignTop: IconBoxAlignTop$1, IconCategory: IconCategory$1, IconCheck: IconCheck$1, IconChevronLeft: IconChevronLeft$1, IconChevronRight: IconChevronRight$1, IconAlertCircle: IconAlertCircle$1, IconCircleCheck: IconCircleCheck$1, IconCircleX: IconCircleX$1, IconHelp: IconHelp$1, IconInfoCircle: IconInfoCircle$1, IconComic1: IconComic1$1, IconComic1Flat: IconComic1Flat$1, IconComic2: IconComic2$1, IconComic2Flat: IconComic2Flat$1, IconComic3: IconComic3$1, IconComic3Flat: IconComic3Flat$1, IconDeviceFloppy: IconDeviceFloppy$1, IconDotsVertical: IconDotsVertical$1, IconEReader1: IconEReader1$1, IconEReader1Flat: IconEReader1Flat$1, IconEReader2: IconEReader2$1, IconEReader2Flat: IconEReader2Flat$1, IconExternalLink: IconExternalLink$1, IconEye: IconEye$1, IconEyeOff: IconEyeOff$1, IconFileDownload: IconFileDownload$1, IconFilePercent: IconFilePercent$1, IconFolderOpen: IconFolderOpen$1, IconHandClick: IconHandClick$1, IconKeyboard: IconKeyboard$1, IconLayoutDashboard: IconLayoutDashboard$1, IconLayoutBottombar: IconLayoutBottombar$1, IconLayoutBottombarInactive: IconLayoutBottombarInactive$1, IconLayoutSidebar: IconLayoutSidebar$1, IconLayoutSidebarInactive: IconLayoutSidebarInactive$1, IconLayoutSidebarRight: IconLayoutSidebarRight$1, IconLayoutSidebarRightInactive: IconLayoutSidebarRightInactive$1, IconListNumbers: IconListNumbers$1, IconLoader2: IconLoader2$1, IconLocationCog: IconLocationCog$1, IconMenu2: IconMenu2$1, IconMenuDeep: IconMenuDeep$1, IconMessage: IconMessage$1, IconMoon: IconMoon$1, IconPage: IconPage$1, IconPageFlat: IconPageFlat$1, IconPalette: IconPalette$1, IconPencil: IconPencil$1, IconPencilCog: IconPencilCog$1, IconPhoto: IconPhoto$1, IconPhotoOff: IconPhotoOff$1, IconPin: IconPin$1, IconPlayerPause: IconPlayerPause$1, IconPlayerPlay: IconPlayerPlay$1, IconRefresh: IconRefresh$1, IconSettings: IconSettings$1, IconSettingsOff: IconSettingsOff$1, IconSpacingHorizontal: IconSpacingHorizontal$1, IconSpacingVertical: IconSpacingVertical$1, IconSun: IconSun$1, IconTrash: IconTrash$1, IconWorldCog: IconWorldCog$1, IconX: IconX$1, IconZoom: IconZoom$1, IconZoomCancel: IconZoomCancel$1, IconZoomIn: IconZoomIn$1, IconZoomInArea: IconZoomInArea$1, IconZoomOut: IconZoomOut$1, IconZoomOutArea: IconZoomOutArea$1, IconZoomPan: IconZoomPan$1 } = _.mapValues(svg_exports, (rawSvg, iconKey) => { return applyColorsToSvg(rawSvg, `icon-tabler-${_.kebabCase(iconKey.replace(/^Icon/, ""))}`); }); //#endregion //#region \0@oxc-project+runtime@0.139.0/helpers/esm/decorate.js function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } //#endregion //#region src/ui/components/Icon.ts var Icon = class Icon extends i$1 { constructor(..._args) { super(..._args); this.name = ""; this.variant = "regular"; this.family = "classic"; this.label = ""; this.size = ""; } static { this.styles = i$3` :host { --mov-icon-size: 1rem; display: inline-flex; align-items: center; justify-content: center; vertical-align: middle; line-height: 1; } :host([hidden]) { display: none; } svg { width: var(--mov-icon-size, 1rem); height: var(--mov-icon-size, 1rem); display: block; color: inherit; /* This will inherit from the host element */ } `; } updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has("name")) if (StyledIcons_exports[toPascalCase(this.name)]) { this.dispatchEvent(new CustomEvent("load", { bubbles: true, composed: true })); this.dispatchEvent(new CustomEvent("wa-load", { bubbles: true, composed: true })); } else { this.dispatchEvent(new CustomEvent("error", { bubbles: true, composed: true })); this.dispatchEvent(new CustomEvent("wa-error", { bubbles: true, composed: true })); } } render() { const styledSvg = StyledIcons_exports[toPascalCase(this.name)]; if (!styledSvg) return A; const style = this.size ? `--mov-icon-size: ${this.size};` : ""; return b$1`${o$4(styledSvg)}`; } }; __decorate([n$1({ type: String })], Icon.prototype, "name", void 0); __decorate([n$1({ type: String, reflect: true })], Icon.prototype, "variant", void 0); __decorate([n$1({ type: String, reflect: true })], Icon.prototype, "family", void 0); __decorate([n$1({ type: String })], Icon.prototype, "label", void 0); __decorate([n$1({ type: String })], Icon.prototype, "size", void 0); Icon = __decorate([t$1("mov-icon")], Icon); //#endregion //#region src/utils/code-tag.ts /** * A simple template literal tag that concatenates the template strings and expressions. * This function is the base for the `html` and `css` tags and does not perform any special processing. * @internal * @param {TemplateStringsArray} raw - The static parts of the template literal. * @param {...unknown} keys - The dynamic expressions embedded in the template literal. * @returns {string} The concatenated string. */ var concatenateTemplateLiteralTag = (raw, ...keys) => keys.length === 0 ? raw[0] : String.raw({ raw }, ...keys); /** * A template literal tag used to identify HTML strings. * This allows IDEs and tools like Prettier to apply proper syntax highlighting and formatting. * @example * const myHtml = html`
Hello, World!
`; */ var html = concatenateTemplateLiteralTag; //#endregion //#region src/utils/css.ts /** * Creates a `; } //#endregion //#region src/ui/styles/toast.css?inline var toast_default = ".mov-toast-stack {\r\n position: fixed;\r\n z-index: 2000;\r\n width: 350px;\r\n max-width: 100vw;\r\n max-height: 100vh;\r\n padding: 1rem;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 0.5rem;\r\n pointer-events: none;\r\n overflow: hidden;\r\n}\r\n\r\n/* Placements */\r\n.mov-toast-stack-top-start {\r\n top: 0;\r\n left: 0;\r\n}\r\n.mov-toast-stack-top-center {\r\n top: 0;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n align-items: center;\r\n}\r\n.mov-toast-stack-top-end {\r\n top: 0;\r\n right: 0;\r\n}\r\n.mov-toast-stack-bottom-start {\r\n bottom: 0;\r\n left: 0;\r\n flex-direction: column-reverse;\r\n}\r\n.mov-toast-stack-bottom-center {\r\n bottom: 0;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n align-items: center;\r\n flex-direction: column-reverse;\r\n}\r\n.mov-toast-stack-bottom-end {\r\n bottom: 0;\r\n right: 0;\r\n flex-direction: column-reverse;\r\n}\r\n\r\n:host {\r\n display: block;\r\n width: 100%;\r\n}\r\n\r\n.mov-toast {\r\n pointer-events: auto;\r\n display: flex;\r\n flex-direction: column;\r\n background-color: var(--theme-background-color);\r\n color: var(--theme-text-color);\r\n border: 1px solid var(--theme-border-color);\r\n border-radius: 0.5rem;\r\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\r\n overflow: hidden;\r\n transition:\r\n transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),\r\n opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1),\r\n visibility 0.3s cubic-bezier(0.4, 0, 0.2, 1);\r\n opacity: 0;\r\n visibility: hidden;\r\n width: 100%;\r\n}\r\n\r\n/* Animation directions based on placement property */\r\n:host([placement$=\"-end\"]) .mov-toast {\r\n transform: translateX(110%);\r\n}\r\n\r\n:host([placement$=\"-start\"]) .mov-toast {\r\n transform: translateX(-110%);\r\n}\r\n\r\n:host([placement=\"top-center\"]) .mov-toast {\r\n transform: translateY(-110%);\r\n}\r\n\r\n:host([placement=\"bottom-center\"]) .mov-toast {\r\n transform: translateY(110%);\r\n}\r\n\r\n:host([open]) .mov-toast {\r\n transform: translate(0, 0);\r\n opacity: 1;\r\n visibility: visible;\r\n}\r\n\r\n.mov-toast-body {\r\n display: flex;\r\n padding: 0.75rem 1rem;\r\n gap: 0.75rem;\r\n align-items: flex-start;\r\n}\r\n\r\n.mov-toast-icon {\r\n flex-shrink: 0;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n font-size: 20px;\r\n margin-top: 0.125rem;\r\n}\r\n\r\n.mov-toast-icon mov-icon {\r\n --mov-icon-size: 1.25rem;\r\n}\r\n\r\n.mov-toast-content {\r\n flex-grow: 1;\r\n display: flex;\r\n flex-direction: column;\r\n gap: 0.125rem;\r\n}\r\n\r\n.mov-toast-title {\r\n font-weight: 600;\r\n font-size: 14px;\r\n line-height: 1.25;\r\n}\r\n\r\n.mov-toast-description {\r\n font-size: 13px;\r\n opacity: 0.8;\r\n line-height: 1.4;\r\n}\r\n\r\n.mov-toast-close {\r\n flex-shrink: 0;\r\n background: none;\r\n border: none;\r\n cursor: pointer;\r\n padding: 0.25rem;\r\n color: inherit;\r\n opacity: 0.5;\r\n transition: opacity 0.2s;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n margin-right: -0.25rem;\r\n}\r\n\r\n.mov-toast-close:hover {\r\n opacity: 1;\r\n}\r\n\r\n/* Variants */\r\n.mov-toast-variant-primary .mov-toast-icon {\r\n color: var(--mov-color-fill-loud);\r\n}\r\n.mov-toast-variant-success .mov-toast-icon {\r\n color: #28a745;\r\n}\r\n.mov-toast-variant-warning .mov-toast-icon {\r\n color: #ffc107;\r\n}\r\n.mov-toast-variant-danger .mov-toast-icon {\r\n color: #dc3545;\r\n}\r\n.mov-toast-variant-neutral .mov-toast-icon {\r\n color: var(--theme-text-color);\r\n}\r\n\r\n.mov-toast-variant-primary {\r\n border-left: 4px solid var(--mov-color-fill-loud);\r\n}\r\n.mov-toast-variant-success {\r\n border-left: 4px solid #28a745;\r\n}\r\n.mov-toast-variant-warning {\r\n border-left: 4px solid #ffc107;\r\n}\r\n.mov-toast-variant-danger {\r\n border-left: 4px solid #dc3545;\r\n}\r\n.mov-toast-variant-neutral {\r\n border-left: 4px solid var(--theme-border-color);\r\n}\r\n"; //#endregion //#region src/ui/components/Toast.ts var Toast = class Toast extends i$1 { constructor(..._args) { super(..._args); this.open = false; this.variant = "primary"; this.duration = 3e3; this.closable = false; this.title = ""; this.description = ""; this.placement = "bottom-end"; } static { this.styles = [r$4(toast_default)]; } /** Shows the toast. */ async show() { if (this.open) return; await this.updateComplete; this.dispatchEvent(new CustomEvent("wa-show", { bubbles: true, composed: true })); this.open = true; if (this.duration < Infinity) this.autoHideTimeout = window.setTimeout(() => this.hide(), this.duration); return new Promise((resolve) => { setTimeout(() => { this.dispatchEvent(new CustomEvent("wa-after-show", { bubbles: true, composed: true })); resolve(); }, 300); }); } /** Hides the toast. */ async hide() { if (!this.open) return; window.clearTimeout(this.autoHideTimeout); this.dispatchEvent(new CustomEvent("wa-hide", { bubbles: true, composed: true })); this.open = false; return new Promise((resolve) => { setTimeout(() => { this.dispatchEvent(new CustomEvent("wa-after-hide", { bubbles: true, composed: true })); this.remove(); resolve(); }, 300); }); } handleCloseClick() { this.hide(); } getDefaultIcon() { if (this.icon) return this.icon; switch (this.variant) { case "success": return "IconCircleCheck"; case "warning": return "IconAlertCircle"; case "danger": return "IconCircleX"; default: return "IconInfoCircle"; } } render() { return b$1` `; } }; __decorate([n$1({ type: Boolean, reflect: true })], Toast.prototype, "open", void 0); __decorate([n$1({ reflect: true })], Toast.prototype, "variant", void 0); __decorate([n$1({ type: Number })], Toast.prototype, "duration", void 0); __decorate([n$1({ type: Boolean })], Toast.prototype, "closable", void 0); __decorate([n$1()], Toast.prototype, "title", void 0); __decorate([n$1()], Toast.prototype, "description", void 0); __decorate([n$1()], Toast.prototype, "icon", void 0); __decorate([n$1({ reflect: true })], Toast.prototype, "placement", void 0); Toast = __decorate([t$1("mov-toast")], Toast); /** * Utility function to show a toast notification. * Manages singleton toast stacks in the DOM for different placements. */ var toast = (options) => { const placement = options.placement || "bottom-end"; const stackClass = `mov-toast-stack-${placement}`; let stack = document.querySelector(`.mov-toast-stack.${stackClass}`); const hostIndex = toast_default.indexOf(":host"); appendStyleSheet("mov-toast-stack-styles", hostIndex > -1 ? toast_default.substring(0, hostIndex).trim() : toast_default); if (!stack) { stack = document.createElement("div"); stack.className = `mov-toast-stack ${stackClass}`; document.body.appendChild(stack); } const toastItem = document.createElement("mov-toast"); let variant = options.variant || "primary"; if (variant === "info") variant = "primary"; if (variant === "error") variant = "danger"; toastItem.variant = variant; toastItem.title = options.title || ""; toastItem.description = options.description || options.message || ""; toastItem.duration = options.duration ?? 3e3; toastItem.closable = options.closable ?? true; toastItem.placement = placement; if (options.icon) toastItem.icon = options.icon; stack.appendChild(toastItem); requestAnimationFrame(() => { toastItem.show(); }); return toastItem; }; toast.info = (options) => toast({ ...options, variant: "primary" }); toast.success = (options) => toast({ ...options, variant: "success" }); toast.warning = (options) => toast({ ...options, variant: "warning" }); toast.error = (options) => toast({ ...options, variant: "danger" }); //#endregion //#region src/utils/diffObj.ts /** biome-ignore-all lint/suspicious/noExplicitAny: must be generic */ /** * Deep diff between two object, using lodash * * We are comparing two objects: A and B. * Object b is newer and similar to A. * We are looking for changes from A to B. * We assume that data types haven't changed (String to Number). * We assume that parent is either an Array or an Object. * * This is the "easier on the programmer, harder on the computer" method. * It's very inefficient. Worst case is something like O(n^n). * But this works, is easier to understand that the "correct" * implementation, and entertained me for a bit. * * * http://stackoverflow.com/questions/31295545/how-to-get-only-the-changed-values-from-two-json-objects * https://gist.github.com/Yimiprod/7ee176597fef230d1451 * https://stackoverflow.com/questions/8572826/generic-deep-diff-between-two-objects * https://qastack.com.br/programming/31683075/how-to-do-a-deep-comparison-between-2-objects-with-lodash * * @template T - The type of the objects being compared. * @param {T} changed - The object with potential changes. * @param {T} original - The original object to compare against. * @returns {Partial>} A new object representing the difference. */ var diffObj = (changed, original) => { const changes = (object, base) => _.transform(object, (result, value, key) => { if (!_.isEqual(value, base[key])) if (_.isObject(value) && _.isObject(base[key]) && !_.isArray(value)) result[key] = changes(value, base[key]); else result[key] = value; }); return changes(changed, original); }; //#endregion //#region src/core/settings.ts /** * Default settings for the script. * @type {ISettings} */ var defaultSettings = { bookmarks: [], colorScheme: "dark", downloadZip: false, enabled: false, fitWidthIfOversize: true, header: "scroll", hidePageControls: false, lazyLoadImages: false, lazyStart: 50, loadMode: "wait", locale: "en_US", maxReload: 5, minZoom: 30, navbar: "bottom", pagination: "disabled", scrollHeight: 25, theme: "#29487D", loadSpeed: "Extreme", viewMode: "WebComic", zoomMode: "percent", zoomStep: 30, zoomValue: 100, keybinds: { SCROLL_UP: [ "up", "W", "num_8" ], SCROLL_DOWN: [ "down", "S", "num_2" ], NEXT_CHAPTER: [ "right", "/", "D", "num_6" ], PREVIOUS_CHAPTER: [ "left", ";", "A", "num_4" ], RETURN_CHAPTER_LIST: [ "backspace", "del", "num_decimal" ], ENLARGE: [ "-", "num_add", "E" ], REDUCE: [ "=", "num_subtract", "Q" ], RESTORE: [ "9", "num_divide", "R" ], FIT_WIDTH: [ "0", "num_multiply", "F" ], FIT_HEIGHT: ["H", "num_0"], SETTINGS: [ "num_divide", "num_5", "X" ], VIEW_MODE_WEBCOMIC: ["C"], VIEW_MODE_VERTICAL: ["V"], VIEW_MODE_LEFT: ["N"], VIEW_MODE_RIGHT: ["B"], VIEW_MODE_GALLERY: ["G"], SCROLL_START: ["space"], INCREASE_SPEED: ["."], DECREASE_SPEED: [","], TOGGLE_CONTROLS: ["L"] } }; /** * Settings overrides for mobile devices. * @type {Partial} */ var mobileSettings = { lazyLoadImages: true, fitWidthIfOversize: true, navbar: "disabled", viewMode: "WebComic", header: "scroll", hidePageControls: true, pagination: "disabled" }; /** * Settings overrides for standalone mode. * @type {Partial} */ var standaloneSettings = { loadSpeed: "All", lazyLoadImages: false, downloadZip: false, theme: "oklch(44.6% 0.043 257.281)" }; /** * Generates the default settings object based on the device type (mobile/desktop) and scope (global/local). * @param {boolean} [global=true] - Whether to get the global or local default settings. * @returns {ISettings} The default settings object. */ function getDefault(global = true) { const baseSettings = { ...defaultSettings, theme: global ? "#29487D" : "#004526" }; let mergedSettings = isMobile() ? _.defaultsDeep(mobileSettings, baseSettings) : baseSettings; if (isStandalone()) mergedSettings = _.defaultsDeep(standaloneSettings, mergedSettings); return mergedSettings; } /** * A customizer function for Lodash's isEqualWith to handle specific * comparison logic for ISettings objects. * * It specifically handles the 'bookmarks' array and the `keybinds` object, * comparing their contents regardless of their order. * * @param value The value from the first object. * @param other The value from the second object. * @param key The key of the property being compared. * @returns A boolean if a custom comparison is made, or undefined to * let Lodash's default comparison take over. */ function areBookmarksEqual(value, other) { if (Array.isArray(value) && Array.isArray(other)) { if (value.length !== other.length) return false; const getBookmarkSortKey = (b) => `${b.url}-${b.date}`; const sortedValue = [...value].sort((a, b) => getBookmarkSortKey(a).localeCompare(getBookmarkSortKey(b))); const sortedOther = [...other].sort((a, b) => getBookmarkSortKey(a).localeCompare(getBookmarkSortKey(b))); return _.isEqual(sortedValue, sortedOther); } } function areKeybindsEqual(value, other) { if (value && typeof value === "object" && other && typeof other === "object") { const valueKeybinds = value; const otherKeybinds = other; const keysA = _.keys(valueKeybinds).sort((a, b) => a.localeCompare(b)); const keysB = _.keys(otherKeybinds).sort((a, b) => a.localeCompare(b)); if (!_.isEqual(keysA, keysB)) return false; for (const k of keysA) { const sortedArrayA = valueKeybinds[k] ? [...valueKeybinds[k]].sort((a, b) => a.localeCompare(b)) : []; const sortedArrayB = otherKeybinds[k] ? [...otherKeybinds[k]].sort((a, b) => a.localeCompare(b)) : []; if (!_.isEqual(sortedArrayA, sortedArrayB)) return false; } return true; } } function compareSettingsCustomizer(value, other, key) { if (key === "bookmarks") return areBookmarksEqual(value, other); if (key === "keybinds") return areKeybindsEqual(value, other); } /** * Compares two ISettings objects to detect changes, handling bookmarks * as an unordered list. Can also compare individual properties if the * optional key is provided. * * @param newSettings The new settings object or value. * @param oldSettings The old settings object or value. * @param key The optional key to use when comparing individual properties. * @returns true if the settings are identical, false otherwise. */ function haveSettingsChanged(newSettings, oldSettings, key) { if (newSettings === oldSettings) return false; if (key) { const tempA = { [key]: newSettings }; const tempB = { [key]: oldSettings }; return !_.isEqualWith(tempA, tempB, compareSettingsCustomizer); } return !_.isEqualWith(newSettings, oldSettings, compareSettingsCustomizer); } var globalSettings = _.defaultsDeep(getGlobalSettings(getDefault()), getDefault()); var localSettings = _.defaultsDeep(getLocalSettings(getDefault(false)), getDefault(false)); /** * Checks if site-specific (local) settings are currently enabled and active. * @returns {boolean} True if local settings are enabled, false otherwise. */ var isSettingsLocal = () => localSettings?.enabled === true; var isLocalSettingsAllowed = (key) => isSettingsLocal() && ![ "locale", "bookmarks", "keybinds" ].includes(key); /** * The reactive store for all settings. It holds either global or local settings based on the current mode. * Components should subscribe to this store to react to settings changes. * @type {import('nanostores').MapStore} */ var settings = /* @__PURE__ */ map(isSettingsLocal() ? { ...localSettings, locale: globalSettings.locale, keybinds: globalSettings.keybinds, bookmarks: globalSettings.bookmarks } : globalSettings); /** * A computed store that provides the currently selected locale object based on the `locale` setting. * @type {import('nanostores').ComputedStore} */ var locale = /* @__PURE__ */ computed(settings, (current) => locales.find((l) => l.ID === current.locale) ?? locales[1]); /** * The reactive store for the application's volatile state, like current page, scroll state, etc. * This state is not persisted. * @type {import('nanostores').MapStore} */ var appState = /* @__PURE__ */ map({ autoScroll: false, chapter: e$6(), currentPage: 0, device: getDevice(), manga: void 0, panel: "none", scrollToPage: void 0 }); /** * Refreshes the reactive `settings` store with the latest values from the raw settings objects. * Call this after a change is made to `globalSettings` or `localSettings` to propagate the change. * @param {ISettingsKey} [key] - If provided, refreshes only a single key. Otherwise, refreshes the entire object. */ function refreshSettings(key) { if (key) { const newVal = isLocalSettingsAllowed(key) ? localSettings[key] : globalSettings[key]; const currentVal = settings.get()?.[key]; if (haveSettingsChanged(currentVal, newVal, key)) { settings.setKey(key, newVal); logScript("Refreshed Settings", key, newVal); } return; } for (const key in settings.get()) { const currentObj = settings.get()[key]; const newObj = isLocalSettingsAllowed(key) ? localSettings[key] : globalSettings[key]; if (haveSettingsChanged(currentObj, newObj)) settings.setKey(key, newObj); } logScript("Refreshed All Settings"); } /** * Synchronization callback for when global settings are changed in another tab. * @param {Partial} newValue - The new settings value from storage. */ function syncGlobalSettings(newValue) { const newSettings = _.defaultsDeep(newValue, getDefault()); const diff = globalSettings ? diffObj(newSettings, globalSettings) : newSettings; if (!isNothing(diff)) { logScript("Imported Global Settings", diff); globalSettings = newSettings; for (const key in diff) refreshSettings(key); } } settingsChangeListener(_.debounce(syncGlobalSettings, 300), "settings"); /** * Synchronization callback for when local settings are changed in another tab. * @param {Partial} newValue - The new settings value from storage. */ function syncLocalSettings(newValue) { const newSettings = _.defaultsDeep(newValue, getDefault(false)); const diff = localSettings ? diffObj(newSettings, localSettings) : newSettings; if (!isNothing(diff)) { logScript("Imported Local Settings", diff); localSettings = newSettings; for (const key in diff) refreshSettings(key); } } settingsChangeListener(_.debounce(syncLocalSettings, 300), location.hostname); /** * Gets the effective value for a setting from the reactive store. * This respects whether local or global settings are currently active. * @template K * @param {K} key - The key of the setting to retrieve. * @returns {ISettings[K]} The effective value of the setting. */ function getSettingsValue(key) { return settings.get()?.[key]; } /** * Sets a single setting value in the reactive `settings` store. This change is NOT persisted to storage. * @template K * @param {K} key - The key of the setting to update. * @param {ISettings[K]} value - The new value for the setting. */ function setSettingsValue(key, value) { const current = settings.get()?.[key]; if (!haveSettingsChanged(current, value, key)) return; settings.setKey(key, value); } /** * Saves a single setting value by its key and persists the change to Tampermonkey's storage. * @template K * @param {K} key - The key of the setting to update. * @param {ISettings[K]} value - The new value for the setting. */ function saveSettingsValue(key, value) { if (!haveSettingsChanged(getSettingsValue(key), value, key)) return; settings.setKey(key, value); if (isLocalSettingsAllowed(key)) { localSettings[key] = value; saveLocalSettings(diffObj(localSettings, getDefault(false))); } else { globalSettings[key] = value; saveGlobalSettings(diffObj(globalSettings, getDefault())); } } /** * Updates a single setting value using a function and persists the change. * @template K * @param {K} key - The key of the setting to update. * @param {(value: ISettings[K]) => ISettings[K]} fn - A function that receives the current value and returns the new value. */ function changeSettingsValue(key, fn) { setSettingsValue(key, fn(getSettingsValue(key))); } /** * Gets a value from the reactive `appState` store. * @template K * @param {K} key - The key of the value to get. * @returns {IApp[K]} The value from the app state. */ function getAppStateValue(key) { return appState.get()[key]; } /** * Sets a value in the reactive `appState` store. * @template K * @param {K} key - The key of the value to set. * @param {IApp[K]} value - The new value. */ function setAppStateValue(key, value) { const current = appState.get()[key]; if (_.isEqual(current, value)) return; appState.setKey(key, value); } /** * Updates a value in the `appState` store using a function. * @template K * @param {K} key - The key of the value to change. * @param {(value: IApp[K]) => IApp[K]} fn - The function to change the value. */ function changeAppStateValue(key, fn) { const oldVal = appState.get()[key]; const newVal = fn(oldVal); if (_.isEqual(oldVal, newVal)) return; appState.setKey(key, newVal); } /** * Updates a specific image's data within the `appState.images` object. * This is useful for updating properties of a single image (e.g., its loaded status, dimensions). * * @param {number} index - The index of the image to update. * @param {(value: Page) => Page} fn - A function that receives the current image data and returns the updated image data. */ function changeImage(index, fn) { changeAppStateValue("images", (images) => ({ ...images, [index]: { ...images?.[index], ...fn(images?.[index] ?? {}) } })); } /** * Gets a translated string from the current locale. * @param {ILocaleKey | string} name - The key of the string to get. * @returns {string} The translated string, or a placeholder if not found. */ function getLocaleString(name) { const currentLocale = locales.find((l) => l.ID === getSettingsValue("locale")) ?? locales[1]; if (isKey(currentLocale, name)) return currentLocale?.[name] ?? locales[1]?.[name]; return `##MISSING_STRING_${name}##`; } /** * Enables or disables site-specific (local) settings. * @param {boolean} [activate=false] - Whether to activate or deactivate local settings. * @returns {boolean} The new state of local settings (true if enabled, false otherwise). */ function toggleLocalSettings(activate = false) { localSettings.enabled = activate; saveLocalSettings(diffObj(localSettings, getDefault(false))); logScript("Local Settings ", activate ? "Enabled" : "Disabled"); toast.info({ title: `Changed Settings to`, description: isSettingsLocal() ? "Local" : "Global", duration: 2e3 }); return isSettingsLocal(); } /** * Resets the settings (either local or global) to their default values. */ function resetSettings() { if (isSettingsLocal()) { removeValueGM(location.hostname); localSettings = getDefault(false); toggleLocalSettings(false); } else { removeValueGM("settings"); globalSettings = getDefault(); refreshSettings(); } logScript("Settings Reset"); } /** * Checks if a URL is bookmarked. * @param {string} [url=window.location.href] - The URL to check. * @returns {number | undefined} The bookmarked page number, or undefined if not bookmarked. */ function isBookmarked(url = location.href) { return getSettingsValue("bookmarks").find((el) => el.url === url)?.page; } /** * A debug utility to log the current state of settings to the console. * @param {ISettingsKey | null} [key=null] - An optional settings key to inspect a specific value. */ function showSettings(key = null) { logScriptVerbose("Current Settings (Local:", isSettingsLocal(), ") ", key ? settings.get()[key] : settings.get(), "\nGlobal Settings", key ? globalSettings[key] : globalSettings, "\nLocal Settings", key ? localSettings[key] : localSettings, "\nAppState", appState.get()); } giveToWindow("MOVSettings", showSettings); var changedSettings = (newSettings, oldSettings, changedKey) => { if (changedKey && !["bookmarks", "zoomValue"].includes(changedKey)) { const oldValue = oldSettings[changedKey]; const newValue = newSettings[changedKey]; toast.info({ title: `${changedKey} Changed`, description: `from ${JSON.stringify(oldValue)} to ${JSON.stringify(newValue)}`, duration: 2e3 }); } }; settings.listen(_.debounce(changedSettings, 300)); //#endregion //#region node_modules/lit-html/directives/if-defined.js /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var o$3 = (o) => o ?? A; //#endregion //#region src/ui/styles/button.css?inline var button_default = ":host {\r\n display: inline-block;\r\n --mov-font-size-scale: 1;\r\n --mov-font-size-m: calc(16px * var(--mov-font-size-scale));\r\n --mov-font-size-s: round(calc(var(--mov-font-size-m) / 1.125), 1px);\r\n --mov-font-size-l: round(calc(var(--mov-font-size-m) * 1.125 * 1.125), 1px);\r\n --mov-border-width-s: 0.0625rem;\r\n --mov-border-radius-pill: 9999px;\r\n --mov-transition-fast: 75ms;\r\n --mov-font-weight-action: 500;\r\n --mov-focus-ring: solid 0.1875rem var(--mov-color-fill-loud);\r\n --mov-focus-ring-offset: 0.0625rem;\r\n --mov-line-height-condensed: 1.2;\r\n --mov-form-control-padding-block: 0.75em;\r\n --mov-form-control-padding-inline: 1em;\r\n --mov-form-control-height: round(\r\n calc(2 * var(--mov-form-control-padding-block) + 1em * var(--mov-line-height-condensed)),\r\n 1px\r\n );\r\n}\r\n\r\n:host([size=\"small\"]) {\r\n font-size: var(--mov-font-size-s);\r\n}\r\n:host([size=\"medium\"]) {\r\n font-size: var(--mov-font-size-m);\r\n}\r\n:host([size=\"large\"]) {\r\n font-size: var(--mov-font-size-l);\r\n}\r\n\r\n.button {\r\n box-sizing: border-box;\r\n display: inline-flex;\r\n align-items: center;\r\n justify-content: center;\r\n text-decoration: none;\r\n user-select: none;\r\n white-space: nowrap;\r\n vertical-align: middle;\r\n transition-property: background, border, box-shadow, color;\r\n transition-duration: var(--mov-transition-fast);\r\n cursor: pointer;\r\n padding: 0 var(--mov-form-control-padding-inline);\r\n font-family: inherit;\r\n font-size: inherit;\r\n font-weight: var(--mov-font-weight-action);\r\n line-height: calc(var(--mov-form-control-height) - var(--mov-border-width-s) * 2);\r\n height: var(--mov-form-control-height);\r\n border-radius: var(--mov-border-radius-m, 0.375rem);\r\n border-style: solid;\r\n border-width: var(--mov-border-width-s);\r\n background-color: var(--mov-color-fill-loud);\r\n color: var(--mov-color-on-loud);\r\n border-color: transparent;\r\n}\r\n\r\n/* Appearance modifiers */\r\n:host([appearance~=\"plain\"]) {\r\n .button {\r\n color: var(--mov-color-on-quiet);\r\n background-color: transparent;\r\n border-color: transparent;\r\n }\r\n @media (hover: hover) {\r\n .button:not(.disabled):not(.loading):hover {\r\n color: var(--mov-color-on-quiet);\r\n background-color: var(--mov-color-fill-quiet);\r\n }\r\n }\r\n .button:not(.disabled):not(.loading):active {\r\n color: var(--mov-color-on-quiet);\r\n background-color: color-mix(in oklab, var(--mov-color-fill-quiet), var(--mov-color-mix-active));\r\n }\r\n}\r\n\r\n:host([appearance~=\"outlined\"]) {\r\n .button {\r\n color: var(--mov-color-on-quiet);\r\n background-color: transparent;\r\n border-color: var(--mov-color-border-loud);\r\n }\r\n @media (hover: hover) {\r\n .button:not(.disabled):not(.loading):hover {\r\n color: var(--mov-color-on-quiet);\r\n background-color: var(--mov-color-fill-quiet);\r\n }\r\n }\r\n .button:not(.disabled):not(.loading):active {\r\n color: var(--mov-color-on-quiet);\r\n background-color: color-mix(in oklab, var(--mov-color-fill-quiet), var(--mov-color-mix-active));\r\n }\r\n}\r\n\r\n:host([appearance~=\"filled\"]) {\r\n .button {\r\n color: var(--mov-color-on-normal);\r\n background-color: var(--mov-color-fill-normal);\r\n border-color: transparent;\r\n }\r\n @media (hover: hover) {\r\n .button:not(.disabled):not(.loading):hover {\r\n color: var(--mov-color-on-normal);\r\n background-color: color-mix(\r\n in oklab,\r\n var(--mov-color-fill-normal),\r\n var(--mov-color-mix-hover)\r\n );\r\n }\r\n }\r\n .button:not(.disabled):not(.loading):active {\r\n color: var(--mov-color-on-normal);\r\n background-color: color-mix(\r\n in oklab,\r\n var(--mov-color-fill-normal),\r\n var(--mov-color-mix-active)\r\n );\r\n }\r\n}\r\n\r\n:host([appearance~=\"filled\"][appearance~=\"outlined\"]) .button {\r\n border-color: var(--mov-color-border-normal);\r\n}\r\n\r\n:host([appearance~=\"accent\"]) {\r\n .button {\r\n color: var(--mov-color-on-loud);\r\n background-color: var(--mov-color-fill-loud);\r\n border-color: transparent;\r\n }\r\n @media (hover: hover) {\r\n .button:not(.disabled):not(.loading):hover {\r\n background-color: color-mix(in oklab, var(--mov-color-fill-loud), var(--mov-color-mix-hover));\r\n }\r\n }\r\n .button:not(.disabled):not(.loading):active {\r\n background-color: color-mix(in oklab, var(--mov-color-fill-loud), var(--mov-color-mix-active));\r\n }\r\n}\r\n/* Focus states */\r\n.button:focus {\r\n outline: none;\r\n}\r\n.button:focus-visible {\r\n outline: var(--mov-focus-ring);\r\n outline-offset: var(--mov-focus-ring-offset);\r\n}\r\n\r\n/* Disabled state */\r\n.button.disabled {\r\n opacity: 0.5;\r\n cursor: not-allowed;\r\n}\r\n.button.disabled * {\r\n pointer-events: none;\r\n}\r\n\r\n/* Icon buttons */\r\n.button.is-icon-button {\r\n outline-offset: 2px;\r\n width: var(--mov-form-control-height);\r\n aspect-ratio: 1;\r\n}\r\n\r\n/* Pill modifier */\r\n:host([pill]) .button {\r\n border-radius: var(--mov-border-radius-pill);\r\n}\r\n\r\n.start,\r\n.end {\r\n flex: 0 0 auto;\r\n display: flex;\r\n align-items: center;\r\n pointer-events: none;\r\n}\r\n\r\n.label {\r\n display: inline-block;\r\n}\r\n.is-icon-button .label {\r\n display: flex;\r\n}\r\n\r\nmov-icon[part~=\"caret\"] {\r\n display: flex;\r\n align-self: center;\r\n align-items: center;\r\n}\r\nmov-icon[part~=\"caret\"]::part(svg) {\r\n width: 0.875em;\r\n height: 0.875em;\r\n}\r\n\r\n.loading {\r\n position: relative;\r\n cursor: wait;\r\n}\r\n.loading .start,\r\n.loading .label,\r\n.loading .end,\r\n.loading .caret {\r\n visibility: hidden;\r\n}\r\n\r\n.spinner {\r\n --indicator-color: currentColor;\r\n --track-color: color-mix(in oklab, currentColor, transparent 90%);\r\n position: absolute;\r\n font-size: 1em;\r\n height: 1em;\r\n width: 1em;\r\n top: calc(50% - 0.5em);\r\n left: calc(50% - 0.5em);\r\n border-radius: 50%;\r\n border: 2px solid var(--track-color);\r\n border-top-color: var(--indicator-color);\r\n animation: spin 1s linear infinite;\r\n}\r\n\r\n@keyframes spin {\r\n to {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n\r\nslot[name=\"start\"]::slotted(*) {\r\n margin-inline-end: 0.75em;\r\n}\r\nslot[name=\"end\"]::slotted(*),\r\n.button:not(.visually-hidden-label) [part~=\"caret\"] {\r\n margin-inline-start: 0.75em;\r\n}\r\n"; //#endregion //#region src/ui/components/Button.ts var Button = class Button extends i$1 { constructor(..._args) { super(..._args); this.isIconButton = false; this.hasLabel = false; this.hasStart = false; this.hasEnd = false; this.title = ""; this.appearance = "accent"; this.variant = "brand"; this.size = "medium"; this.withCaret = false; this.disabled = false; this.loading = false; this.pill = false; this.type = "button"; } static { this.styles = [r$4(button_default)]; } handleClick(event) { if (this.disabled || this.loading) { event.preventDefault(); event.stopPropagation(); } } click() { this.button?.click(); } focus(options) { this.button?.focus(options); } blur() { this.button?.blur(); } render() { const isLink = !!this.href; const classes = { button: true, "with-caret": this.withCaret, disabled: this.disabled, loading: this.loading, pill: this.pill, "has-label": this.hasLabel, "has-start": this.hasStart, "has-end": this.hasEnd, "is-icon-button": this.isIconButton }; const buttonContent = b$1` ${this.withCaret ? b$1`` : ""} ${this.loading ? b$1`` : ""} `; if (isLink) return b$1` ${buttonContent} `; else return b$1` `; } handleLabelSlotChange() { const nodes = this.labelSlot?.assignedNodes({ flatten: true }) ?? []; const elements = nodes.filter((node) => node.nodeType === Node.ELEMENT_NODE); const textNodes = nodes.filter((node) => node.nodeType === Node.TEXT_NODE && node.textContent?.trim() !== ""); const isIcon = (el) => [ "wa-icon", "mov-icon", "svg" ].includes(el.localName); const hasIcon = elements.some(isIcon); this.isIconButton = textNodes.length === 0 && hasIcon; } }; __decorate([e$2(".button")], Button.prototype, "button", void 0); __decorate([e$2("slot:not([name])")], Button.prototype, "labelSlot", void 0); __decorate([r$1()], Button.prototype, "isIconButton", void 0); __decorate([r$1()], Button.prototype, "hasLabel", void 0); __decorate([r$1()], Button.prototype, "hasStart", void 0); __decorate([r$1()], Button.prototype, "hasEnd", void 0); __decorate([n$1()], Button.prototype, "title", void 0); __decorate([n$1({ reflect: true })], Button.prototype, "appearance", void 0); __decorate([n$1({ reflect: true })], Button.prototype, "variant", void 0); __decorate([n$1({ reflect: true })], Button.prototype, "size", void 0); __decorate([n$1({ attribute: "with-caret", type: Boolean, reflect: true })], Button.prototype, "withCaret", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Button.prototype, "disabled", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Button.prototype, "loading", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Button.prototype, "pill", void 0); __decorate([n$1()], Button.prototype, "type", void 0); __decorate([n$1({ reflect: true })], Button.prototype, "name", void 0); __decorate([n$1({ reflect: true })], Button.prototype, "value", void 0); __decorate([n$1({ reflect: true })], Button.prototype, "href", void 0); __decorate([n$1()], Button.prototype, "target", void 0); __decorate([n$1({ reflect: true })], Button.prototype, "rel", void 0); __decorate([n$1()], Button.prototype, "download", void 0); __decorate([n$1({ reflect: true })], Button.prototype, "form", void 0); Button = __decorate([t$1("mov-button")], Button); //#endregion //#region src/ui/components/ToggleButton.ts var ToggleButton = class ToggleButton extends i$1 { constructor(..._args) { super(..._args); this.mode = "burger"; this.active = false; this.label = ""; this.icon = ""; this.activeIcon = ""; this.appearance = "accent"; this.size = "medium"; this.disabled = false; this.loading = false; } static { this.styles = i$3` :host { display: inline-flex; vertical-align: middle; --burger-size: 1.25rem; --burger-line-height: 2px; --burger-line-color: currentColor; --burger-transition-duration: 0.3s; } /* Base button styling */ mov-button { position: relative; } /* Single icon modes - simple rotation in place */ .single-icon-mode mov-icon { transition: transform 0.3s ease; display: block; } .chevron-icon { transform: rotate(0deg); } :host([active]) .chevron-icon { transform: rotate(90deg); } .expand-icon { transform: rotate(0deg); } :host([active]) .expand-icon { transform: rotate(180deg); } /* Two icon modes - positioned for smooth swap */ .two-icon-mode { position: relative; } .two-icon-mode mov-icon { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); transition: opacity 0.25s ease, transform 0.3s ease; } /* Default state: inactive visible, active hidden */ .inactive-icon { opacity: 1; transform: translate(-50%, -50%) scale(1); } .active-icon { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } /* Active state: inactive hidden, active visible */ :host([active]) .inactive-icon { opacity: 0; transform: translate(-50%, -50%) scale(0.8); } :host([active]) .active-icon { opacity: 1; transform: translate(-50%, -50%) scale(1); } /* Play-pause uses single icon swap without positioning issues */ .play-pause-icon { transition: opacity 0.2s ease; display: block; } /* Burger Mode Styling */ .burger-mode { display: flex; flex-direction: column; justify-content: center; align-items: center; width: var(--burger-size); height: var(--burger-size); position: relative; } .burger-line { width: 100%; height: var(--burger-line-height); background-color: var(--burger-line-color); border-radius: var(--burger-line-height); transition: transform var(--burger-transition-duration) ease, opacity var(--burger-transition-duration) ease; position: absolute; } .burger-line:nth-child(1) { transform: translateY(-6px); } .burger-line:nth-child(2) { transform: translateY(0); } .burger-line:nth-child(3) { transform: translateY(6px); } :host([active]) .burger-line:nth-child(1) { transform: translateY(0) rotate(45deg); } :host([active]) .burger-line:nth-child(2) { opacity: 0; transform: translateX(4px); } :host([active]) .burger-line:nth-child(3) { transform: translateY(0) rotate(-45deg); } /* Size adjustments for burger */ :host([size="small"]) { --burger-size: 1rem; } :host([size="large"]) { --burger-size: 1.5rem; } /* Simple click feedback without disrupting layout */ mov-button:active { transform: scale(0.96); } /* Loading state */ :host([loading]) mov-icon { animation: spin 1s linear infinite; } @keyframes spin { to { transform: rotate(360deg); } } /* Hover effects */ mov-button:hover:not(:disabled) { filter: brightness(1.05); } /* Focus visible enhancement */ mov-button:focus-visible { outline: 2px solid var(--mov-color-fill-loud, currentColor); outline-offset: 2px; } /* Ensure proper centering for all modes */ mov-button.single-icon-mode { display: flex; align-items: center; justify-content: center; } /* Fix icon sizing consistency */ mov-icon { flex-shrink: 0; } `; } connectedCallback() { super.connectedCallback(); if (!this.label) this.label = this._getDefaultLabel(); } render() { const currentLabel = this.active ? this.activeLabel ?? this.label : this.label; const classes = { "two-icon-mode": ["custom", "theme"].includes(this.mode), "single-icon-mode": [ "chevron", "expand", "play-pause" ].includes(this.mode), "burger-mode-active": this.mode === "burger" }; return b$1` ${this._renderIcons()} `; } _getDefaultLabel() { switch (this.mode) { case "burger": return "Toggle menu"; case "chevron": return "Toggle expand"; case "theme": return "Toggle theme"; case "play-pause": return "Toggle play"; case "expand": return "Toggle expand"; case "custom": return "Toggle"; default: return "Toggle"; } } _getIcons() { switch (this.mode) { case "chevron": return { inactive: "chevron-right", active: "chevron-right" }; case "theme": return { inactive: "moon", active: "sun" }; case "play-pause": return { inactive: "player-play", active: "player-pause" }; case "expand": return { inactive: "arrow-autofit-down", active: "arrow-autofit-down" }; case "custom": return { inactive: this.icon, active: this.activeIcon }; default: return { inactive: "", active: "" }; } } _renderIcons() { if (this.mode === "burger") return b$1`
`; const icons = this._getIcons(); if (!icons.inactive) return A; if (this.mode === "chevron") return b$1``; if (this.mode === "expand") return b$1``; if (this.mode === "play-pause") return b$1``; return b$1` `; } _onClick() { if (this.disabled || this.loading) return; const oldActive = this.active; this.active = !this.active; this.dispatchEvent(new CustomEvent("toggle", { detail: { value: this.active, oldValue: oldActive, mode: this.mode }, bubbles: true, composed: true })); } /** Programmatically triggers the toggle action. */ toggle() { this._onClick(); } /** * Sets the active state of the button without dispatching a `toggle` event. * @param {boolean} active - The new active state. */ setActive(active) { this.active = active; } }; __decorate([n$1({ type: String })], ToggleButton.prototype, "mode", void 0); __decorate([n$1({ type: Boolean, reflect: true })], ToggleButton.prototype, "active", void 0); __decorate([n$1({ type: String })], ToggleButton.prototype, "label", void 0); __decorate([n$1({ type: String })], ToggleButton.prototype, "activeLabel", void 0); __decorate([n$1({ type: String })], ToggleButton.prototype, "icon", void 0); __decorate([n$1({ type: String })], ToggleButton.prototype, "activeIcon", void 0); __decorate([n$1({ type: String, reflect: true })], ToggleButton.prototype, "appearance", void 0); __decorate([n$1({ type: String, reflect: true })], ToggleButton.prototype, "size", void 0); __decorate([n$1({ type: Boolean })], ToggleButton.prototype, "disabled", void 0); __decorate([n$1({ type: Boolean, reflect: true })], ToggleButton.prototype, "loading", void 0); ToggleButton = __decorate([t$1("toggle-button")], ToggleButton); //#endregion //#region node_modules/lit-html/directives/style-map.js /** * @license * Copyright 2018 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var n = "important"; var i = " !important"; var o$2 = e$7(class extends i$4 { constructor(t) { if (super(t), t.type !== t$3.ATTRIBUTE || "style" !== t.name || t.strings?.length > 2) throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute."); } render(t) { return Object.keys(t).reduce((e, r) => { const s = t[r]; return null == s ? e : e + `${r = r.includes("-") ? r : r.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g, "-$&").toLowerCase()}:${s};`; }, ""); } update(e, [r]) { const { style: s } = e.element; if (void 0 === this.ft) return this.ft = new Set(Object.keys(r)), this.render(r); for (const t of this.ft) r[t] ?? (this.ft.delete(t), t.includes("-") ? s.removeProperty(t) : s[t] = null); for (const t in r) { const e = r[t]; if (null != e) { this.ft.add(t); const r = "string" == typeof e && e.endsWith(i); t.includes("-") || r ? s.setProperty(t, r ? e.slice(0, -11) : e, r ? n : "") : s[t] = e; } } return E; } }); //#endregion //#region src/utils/colors.ts /** * A collection of sample colors, courtesy of colors.css. */ var sample = { navy: "#001f3f", darkblue: "#1e4f7a", blue: "#1A2F4B", darkgreen: "#062925", green: "#1A3636", grass: "#1B3C53", teal: "#044A42", darkpurple: "#1B0044", purple: "#363062", grape: "#31326F", maroon: "#44000D" }; /** * A comprehensive collection of predefined color palettes. * @type {IPalette} */ var colors = { dark: { 50: "#C1C2C5", 100: "#A6A7AB", 200: "#909296", 300: "#5c5f66", 400: "#373A40", 500: "#2C2E33", 600: "#25262b", 700: "#1A1B1E", 800: "#141517", 900: "#101113", 950: "#000000" }, slate: { "50": "oklch(98.4% 0.003 247.858)", "100": "oklch(96.8% 0.007 247.896)", "200": "oklch(92.9% 0.013 255.508)", "300": "oklch(86.9% 0.022 252.894)", "400": "oklch(70.4% 0.04 256.788)", "500": "oklch(55.4% 0.046 257.417)", "600": "oklch(44.6% 0.043 257.281)", "700": "oklch(37.2% 0.044 257.287)", "800": "oklch(27.9% 0.041 260.031)", "900": "oklch(20.8% 0.042 265.755)", "950": "oklch(12.9% 0.042 264.695)" }, gray: { "50": "oklch(98.5% 0.002 247.839)", "100": "oklch(96.7% 0.003 264.542)", "200": "oklch(92.8% 0.006 264.531)", "300": "oklch(87.2% 0.01 258.338)", "400": "oklch(70.7% 0.022 261.325)", "500": "oklch(55.1% 0.027 264.364)", "600": "oklch(44.6% 0.03 256.802)", "700": "oklch(37.3% 0.034 259.733)", "800": "oklch(27.8% 0.033 256.848)", "900": "oklch(21% 0.034 264.665)", "950": "oklch(13% 0.028 261.692)" }, zinc: { "50": "oklch(98.5% 0 0)", "100": "oklch(96.7% 0.001 286.375)", "200": "oklch(92% 0.004 286.32)", "300": "oklch(87.1% 0.006 286.286)", "400": "oklch(70.5% 0.015 286.067)", "500": "oklch(55.2% 0.016 285.938)", "600": "oklch(44.2% 0.017 285.786)", "700": "oklch(37% 0.013 285.805)", "800": "oklch(27.4% 0.006 286.033)", "900": "oklch(21% 0.006 285.885)", "950": "oklch(14.1% 0.005 285.823)" }, neutral: { "50": "oklch(98.5% 0 0)", "100": "oklch(97% 0 0)", "200": "oklch(92.2% 0 0)", "300": "oklch(87% 0 0)", "400": "oklch(70.8% 0 0)", "500": "oklch(55.6% 0 0)", "600": "oklch(43.9% 0 0)", "700": "oklch(37.1% 0 0)", "800": "oklch(26.9% 0 0)", "900": "oklch(20.5% 0 0)", "950": "oklch(14.5% 0 0)" }, stone: { "50": "oklch(98.5% 0.001 106.423)", "100": "oklch(97% 0.001 106.424)", "200": "oklch(92.3% 0.003 48.717)", "300": "oklch(86.9% 0.005 56.366)", "400": "oklch(70.9% 0.01 56.259)", "500": "oklch(55.3% 0.013 58.071)", "600": "oklch(44.4% 0.011 73.639)", "700": "oklch(37.4% 0.01 67.558)", "800": "oklch(26.8% 0.007 34.298)", "900": "oklch(21.6% 0.006 56.043)", "950": "oklch(14.7% 0.004 49.25)" }, red: { "50": "oklch(97.1% 0.013 17.38)", "100": "oklch(93.6% 0.032 17.717)", "200": "oklch(88.5% 0.062 18.334)", "300": "oklch(80.8% 0.114 19.571)", "400": "oklch(70.4% 0.191 22.216)", "500": "oklch(63.7% 0.237 25.331)", "600": "oklch(57.7% 0.245 27.325)", "700": "oklch(50.5% 0.213 27.518)", "800": "oklch(44.4% 0.177 26.899)", "900": "oklch(39.6% 0.141 25.723)", "950": "oklch(25.8% 0.092 26.042)" }, orange: { "50": "oklch(98% 0.016 73.684)", "100": "oklch(95.4% 0.038 75.164)", "200": "oklch(90.1% 0.076 70.697)", "300": "oklch(83.7% 0.128 66.29)", "400": "oklch(75% 0.183 55.934)", "500": "oklch(70.5% 0.213 47.604)", "600": "oklch(64.6% 0.222 41.116)", "700": "oklch(55.3% 0.195 38.402)", "800": "oklch(47% 0.157 37.304)", "900": "oklch(40.8% 0.123 38.172)", "950": "oklch(26.6% 0.079 36.259)" }, amber: { "50": "oklch(98.7% 0.022 95.277)", "100": "oklch(96.2% 0.059 95.617)", "200": "oklch(92.4% 0.12 95.746)", "300": "oklch(87.9% 0.169 91.605)", "400": "oklch(82.8% 0.189 84.429)", "500": "oklch(76.9% 0.188 70.08)", "600": "oklch(66.6% 0.179 58.318)", "700": "oklch(55.5% 0.163 48.998)", "800": "oklch(47.3% 0.137 46.201)", "900": "oklch(41.4% 0.112 45.904)", "950": "oklch(27.9% 0.077 45.635)" }, yellow: { "50": "oklch(98.7% 0.026 102.212)", "100": "oklch(97.3% 0.071 103.193)", "200": "oklch(94.5% 0.129 101.54)", "300": "oklch(90.5% 0.182 98.111)", "400": "oklch(85.2% 0.199 91.936)", "500": "oklch(79.5% 0.184 86.047)", "600": "oklch(68.1% 0.162 75.834)", "700": "oklch(55.4% 0.135 66.442)", "800": "oklch(47.6% 0.114 61.907)", "900": "oklch(42.1% 0.095 57.708)", "950": "oklch(28.6% 0.066 53.813)" }, lime: { "50": "oklch(98.6% 0.031 120.757)", "100": "oklch(96.7% 0.067 122.328)", "200": "oklch(93.8% 0.127 124.321)", "300": "oklch(89.7% 0.196 126.665)", "400": "oklch(84.1% 0.238 128.85)", "500": "oklch(76.8% 0.233 130.85)", "600": "oklch(64.8% 0.2 131.684)", "700": "oklch(53.2% 0.157 131.589)", "800": "oklch(45.3% 0.124 130.933)", "900": "oklch(40.5% 0.101 131.063)", "950": "oklch(27.4% 0.072 132.109)" }, green: { "50": "oklch(98.2% 0.018 155.826)", "100": "oklch(96.2% 0.044 156.743)", "200": "oklch(92.5% 0.084 155.995)", "300": "oklch(87.1% 0.15 154.449)", "400": "oklch(79.2% 0.209 151.711)", "500": "oklch(72.3% 0.219 149.579)", "600": "oklch(62.7% 0.194 149.214)", "700": "oklch(52.7% 0.154 150.069)", "800": "oklch(44.8% 0.119 151.328)", "900": "oklch(39.3% 0.095 152.535)", "950": "oklch(26.6% 0.065 152.934)" }, emerald: { "50": "oklch(97.9% 0.021 166.113)", "100": "oklch(95% 0.052 163.051)", "200": "oklch(90.5% 0.093 164.15)", "300": "oklch(84.5% 0.143 164.978)", "400": "oklch(76.5% 0.177 163.223)", "500": "oklch(69.6% 0.17 162.48)", "600": "oklch(59.6% 0.145 163.225)", "700": "oklch(50.8% 0.118 165.612)", "800": "oklch(43.2% 0.095 166.913)", "900": "oklch(37.8% 0.077 168.94)", "950": "oklch(26.2% 0.051 172.552)" }, teal: { "50": "oklch(98.4% 0.014 180.72)", "100": "oklch(95.3% 0.051 180.801)", "200": "oklch(91% 0.096 180.426)", "300": "oklch(85.5% 0.138 181.071)", "400": "oklch(77.7% 0.152 181.912)", "500": "oklch(70.4% 0.14 182.503)", "600": "oklch(60% 0.118 184.704)", "700": "oklch(51.1% 0.096 186.391)", "800": "oklch(43.7% 0.078 188.216)", "900": "oklch(38.6% 0.063 188.416)", "950": "oklch(27.7% 0.046 192.524)" }, cyan: { "50": "oklch(98.4% 0.019 200.873)", "100": "oklch(95.6% 0.045 203.388)", "200": "oklch(91.7% 0.08 205.041)", "300": "oklch(86.5% 0.127 207.078)", "400": "oklch(78.9% 0.154 211.53)", "500": "oklch(71.5% 0.143 215.221)", "600": "oklch(60.9% 0.126 221.723)", "700": "oklch(52% 0.105 223.128)", "800": "oklch(45% 0.085 224.283)", "900": "oklch(39.8% 0.07 227.392)", "950": "oklch(30.2% 0.056 229.695)" }, sky: { "50": "oklch(97.7% 0.013 236.62)", "100": "oklch(95.1% 0.026 236.824)", "200": "oklch(90.1% 0.058 230.902)", "300": "oklch(82.8% 0.111 230.318)", "400": "oklch(74.6% 0.16 232.661)", "500": "oklch(68.5% 0.169 237.323)", "600": "oklch(58.8% 0.158 241.966)", "700": "oklch(50% 0.134 242.749)", "800": "oklch(44.3% 0.11 240.79)", "900": "oklch(39.1% 0.09 240.876)", "950": "oklch(29.3% 0.066 243.157)" }, blue: { "50": "oklch(97% 0.014 254.604)", "100": "oklch(93.2% 0.032 255.585)", "200": "oklch(88.2% 0.059 254.128)", "300": "oklch(80.9% 0.105 251.813)", "400": "oklch(70.7% 0.165 254.624)", "500": "oklch(62.3% 0.214 259.815)", "600": "oklch(54.6% 0.245 262.881)", "700": "oklch(48.8% 0.243 264.376)", "800": "oklch(42.4% 0.199 265.638)", "900": "oklch(37.9% 0.146 265.522)", "950": "oklch(28.2% 0.091 267.935)" }, indigo: { "50": "oklch(96.2% 0.018 272.314)", "100": "oklch(93% 0.034 272.788)", "200": "oklch(87% 0.065 274.039)", "300": "oklch(78.5% 0.115 274.713)", "400": "oklch(67.3% 0.182 276.935)", "500": "oklch(58.5% 0.233 277.117)", "600": "oklch(51.1% 0.262 276.966)", "700": "oklch(45.7% 0.24 277.023)", "800": "oklch(39.8% 0.195 277.366)", "900": "oklch(35.9% 0.144 278.697)", "950": "oklch(25.7% 0.09 281.288)" }, violet: { "50": "oklch(96.9% 0.016 293.756)", "100": "oklch(94.3% 0.029 294.588)", "200": "oklch(89.4% 0.057 293.283)", "300": "oklch(81.1% 0.111 293.571)", "400": "oklch(70.2% 0.183 293.541)", "500": "oklch(60.6% 0.25 292.717)", "600": "oklch(54.1% 0.281 293.009)", "700": "oklch(49.1% 0.27 292.581)", "800": "oklch(43.2% 0.232 292.759)", "900": "oklch(38% 0.189 293.745)", "950": "oklch(28.3% 0.141 291.089)" }, purple: { "50": "oklch(97.7% 0.014 308.299)", "100": "oklch(94.6% 0.033 307.174)", "200": "oklch(90.2% 0.063 306.703)", "300": "oklch(82.7% 0.119 306.383)", "400": "oklch(71.4% 0.203 305.504)", "500": "oklch(62.7% 0.265 303.9)", "600": "oklch(55.8% 0.288 302.321)", "700": "oklch(49.6% 0.265 301.924)", "800": "oklch(43.8% 0.218 303.724)", "900": "oklch(38.1% 0.176 304.987)", "950": "oklch(29.1% 0.149 302.717)" }, fuchsia: { "50": "oklch(97.7% 0.017 320.058)", "100": "oklch(95.2% 0.037 318.852)", "200": "oklch(90.3% 0.076 319.62)", "300": "oklch(83.3% 0.145 321.434)", "400": "oklch(74% 0.238 322.16)", "500": "oklch(66.7% 0.295 322.15)", "600": "oklch(59.1% 0.293 322.896)", "700": "oklch(51.8% 0.253 323.949)", "800": "oklch(45.2% 0.211 324.591)", "900": "oklch(40.1% 0.17 325.612)", "950": "oklch(29.3% 0.136 325.661)" }, pink: { "50": "oklch(97.1% 0.014 343.198)", "100": "oklch(94.8% 0.028 342.258)", "200": "oklch(89.9% 0.061 343.231)", "300": "oklch(82.3% 0.12 346.018)", "400": "oklch(71.8% 0.202 349.761)", "500": "oklch(65.6% 0.241 354.308)", "600": "oklch(59.2% 0.249 0.584)", "700": "oklch(52.5% 0.223 3.958)", "800": "oklch(45.9% 0.187 3.815)", "900": "oklch(40.8% 0.153 2.432)", "950": "oklch(28.4% 0.109 3.907)" }, rose: { "50": "oklch(96.9% 0.015 12.422)", "100": "oklch(94.1% 0.03 12.58)", "200": "oklch(89.2% 0.058 10.001)", "300": "oklch(81% 0.117 11.638)", "400": "oklch(71.2% 0.194 13.428)", "500": "oklch(64.5% 0.246 16.439)", "600": "oklch(58.6% 0.253 17.585)", "700": "oklch(51.4% 0.222 16.935)", "800": "oklch(45.5% 0.188 13.697)", "900": "oklch(41% 0.159 10.272)", "950": "oklch(27.1% 0.105 12.094)" } }; /** * Determines if a given color is considered "dark" based on its contrast with white and black. * Uses the APCA (Accessible Perceptual Contrast Algorithm) for contrast calculation. * @param {string} color - The color string to evaluate. * @returns {boolean} True if the color is dark (i.e., white text provides better contrast), false otherwise. */ function isDark(color) { try { Color.get(color); } catch (_e) { return true; } return Color.contrast(color, "white", "Lstar") > Color.contrast(color, "black", "Lstar"); } /** * Determines the optimal text color (black or white) for a given background color * to ensure sufficient contrast. * @param {string} color - The background color string. * @returns {string} Either '#FFFFFF' (white) or '#000000' (black). */ function getTextColor(color) { return isDark(color) ? "#FFFFFF" : "#000000"; } /** * A collection of all processed icons, exported as Lit `unsafeSVG` directives. * Use these for direct rendering within a Lit template. * @example html`${IconFileDownload}` */ var { IconAdjustmentsHorizontal, IconApiBook, IconArrowsHorizontal, IconArrowsLeftRight, IconArrowAutofitDown, IconArrowAutofitHeight, IconArrowAutofitLeft, IconArrowAutofitRight, IconArrowAutofitWidth, IconArrowBigLeft, IconArrowBigRight, IconArrowsMove, IconArrowsMoveVertical, IconArrowsVertical, IconBook, IconBookOff, IconBookArrowLeft, IconBookArrowRight, IconBooksReturn, IconBookUpload, IconBookmark, IconBookmarkOff, IconBookmarks, IconBoxAlignTop, IconCategory, IconCheck, IconChevronLeft, IconChevronRight, IconAlertCircle, IconCircleCheck, IconCircleX, IconHelp, IconInfoCircle, IconComic1, IconComic1Flat, IconComic2, IconComic2Flat, IconComic3, IconComic3Flat, IconDeviceFloppy, IconDotsVertical, IconEReader1, IconEReader1Flat, IconEReader2, IconEReader2Flat, IconExternalLink, IconEye, IconEyeOff, IconFileDownload, IconFilePercent, IconFolderOpen, IconHandClick, IconKeyboard, IconLayoutDashboard, IconLayoutBottombar, IconLayoutBottombarInactive, IconLayoutSidebar, IconLayoutSidebarInactive, IconLayoutSidebarRight, IconLayoutSidebarRightInactive, IconListNumbers, IconLoader2, IconLocationCog, IconMenu2, IconMenuDeep, IconMessage, IconMoon, IconPage, IconPageFlat, IconPalette, IconPencil, IconPencilCog, IconPhoto, IconPhotoOff, IconPin, IconPlayerPause, IconPlayerPlay, IconRefresh, IconSettings, IconSettingsOff, IconSpacingHorizontal, IconSpacingVertical, IconSun, IconTrash, IconWorldCog, IconX, IconZoom, IconZoomCancel, IconZoomIn, IconZoomInArea, IconZoomOut, IconZoomOutArea, IconZoomPan } = _.fromPairs(_.entries(StyledIcons_exports).map(([iconKey, icon]) => [iconKey, o$4(icon)])); //#endregion //#region src/ui/components/ColorSwatch.ts var ColorSwatch = class ColorSwatch extends i$1 { constructor(..._args) { super(..._args); this.color = "#000000"; this.size = 26; this.radius = "50%"; this.contrastColor = "#FFFFFF"; this.checked = false; } static { this.styles = i$3` :host { display: inline-flex; align-items: center; justify-content: center; cursor: pointer; position: relative; box-sizing: border-box; -webkit-tap-highlight-color: transparent; } .swatch { position: relative; width: 100%; height: 100%; border-radius: var(--radius); background-color: var(--color); display: flex; align-items: center; justify-content: center; transition: transform 0.15s ease; box-sizing: border-box; border: 1px solid var(--theme-border-color, rgba(0, 0, 0, 0.1)); color: var(--contrast-color); } :host(:hover) .swatch { transform: scale(1.1); } ::slotted(*) { width: 60%; height: 60%; } .check-icon { position: absolute; top: 0; left: 0; right: 0; bottom: 0; color: var(--contrast-color); opacity: 0; transition: opacity 0.15s ease; width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 16px; line-height: 1; } .check-icon svg { width: 60%; height: 60%; } :host([checked]) .check-icon { opacity: 1; } `; } /** * Recalculates the contrasting color for the checkmark whenever the swatch color changes. * @internal */ willUpdate(changedProperties) { if (changedProperties.has("color")) this.contrastColor = getTextColor(this.color); if (changedProperties.has("selected")) this.checked = this.color.toLowerCase() === this.selected?.toLowerCase(); } /** * Handles the click event on the swatch. * Dispatches 'input' and 'change' events with the swatch's color value. * @private */ handleClick() { this.dispatchEvent(new CustomEvent("input", { detail: { value: this.color }, bubbles: true, composed: true })); this.dispatchEvent(new CustomEvent("change", { detail: { value: this.color }, bubbles: true, composed: true })); } render() { const hostStyles = { width: `${this.size}px`, height: `${this.size}px` }; const swatchStyles = { "--radius": typeof this.radius === "number" ? `${this.radius}px` : this.radius, "--color": this.color, "--contrast-color": this.contrastColor }; return b$1`
${IconCheck}
`; } }; __decorate([n$1({ type: String })], ColorSwatch.prototype, "color", void 0); __decorate([n$1({ type: String })], ColorSwatch.prototype, "selected", void 0); __decorate([n$1({ type: Number })], ColorSwatch.prototype, "size", void 0); __decorate([n$1({ type: String })], ColorSwatch.prototype, "radius", void 0); __decorate([n$1({ state: true })], ColorSwatch.prototype, "contrastColor", void 0); __decorate([n$1({ type: Boolean, reflect: true })], ColorSwatch.prototype, "checked", void 0); ColorSwatch = __decorate([t$1("color-swatch")], ColorSwatch); //#endregion //#region src/utils/palettes.ts /** * Generates a consistent 11-shade tonal palette from a single base color. * Any color from the generated palette will produce the same palette when used as a base. * The base color is guaranteed to be in the palette. * @internal * @param {Color} baseColor - A `Color` instance for the base color. * @returns {string[]} An array of 11 hex color strings. */ function gradientBySteps(baseColor) { const [originalLightness, chroma, hue] = baseColor.to("oklch").coords.map((c) => c ?? 0); const lightnessSteps = [ .95, .9, .8, .7, .6, .5, .4, .3, .2, .1, .05 ]; const palette = lightnessSteps.map((l) => new Color("oklch", [ l, chroma, hue ]).toString({ format: "hex" })); let closestIndex = -1; let minDiff = Infinity; for (let i = 0; i < lightnessSteps.length; i++) { const diff = Math.abs(lightnessSteps[i] - originalLightness); if (diff < minDiff) { minDiff = diff; closestIndex = i; } } if (closestIndex !== -1) palette[closestIndex] = baseColor.toString({ format: "hex" }); return palette.map((c) => c.toUpperCase()); } /** * Generates a gradient with adaptive saturation for a more aesthetically pleasing result. * Lighter shades are desaturated, while darker shades have their saturation slightly boosted. * @internal * @param {Color} baseColor - A `Color` instance for the base color. * @returns {string[]} An array of 11 hex color strings. */ function gradientBySaturation(baseColor) { const baseHsl = baseColor.to("hsl"); const lightnessScale = [ .97, .9, .8, .7, .6, .5, .4, .3, .2, .1, .05 ]; const colors = []; for (const l of lightnessScale) { const newColor = baseHsl.clone(); newColor.coords[2] = l * 100; const s = newColor.coords[1] ?? 0; if (l > .8) newColor.coords[1] = s * .4; else if (l > .6) newColor.coords[1] = s * .8; else if (l < .3) newColor.coords[1] = Math.min(100, s * 1.1); colors.push(newColor.toString({ format: "hex" }).toUpperCase()); } return colors; } /** * Generates a gradient using a fixed set of lightness steps. * @internal * @param {Color} baseColor - A `Color` instance for the base color. * @returns {string[]} An array of 11 hex color strings. */ function gradientByLightness(baseColor) { const colors = []; const lightnessSteps = [ 95, 90, 80, 70, 60, 50, 40, 30, 20, 10, 5 ]; const baseHsl = baseColor.to("hsl"); for (const lightness of lightnessSteps) { const c = baseHsl.clone(); c.coords[2] = lightness; colors.push(c.toString({ format: "hex" }).toUpperCase()); } return colors; } /** * Generates an 11-color palette from a single base color, inspired by Chakra UI's color scale. * @param {Color} baseColor - A `Color` instance for the base color. * @returns {string[]} An array of 11 hex color strings. */ function gradientByChakra(baseColor) { const palette = new Array(11).fill(""); const baseHsl = baseColor.to("hsl"); const config = { lightest: { lightness: 95, rotate: -10, saturate: -30 }, darkest: { lightness: 10, rotate: 10, saturate: 10 } }; const lightStepsCount = 5; const darkStepsCount = 5; const lightnessStep = (config.lightest.lightness - 50) / lightStepsCount; const darknessStep = (50 - config.darkest.lightness) / darkStepsCount; const lightRotateStep = config.lightest.rotate / lightStepsCount; const darkRotateStep = config.darkest.rotate / darkStepsCount; const lightSaturateStep = config.lightest.saturate / lightStepsCount; const darkSaturateStep = config.darkest.saturate / darkStepsCount; for (let i = 1; i <= lightStepsCount; i++) { const step = lightStepsCount - i; const color = baseHsl.clone(); color.coords[2] = (color.coords[2] ?? 0) + lightnessStep * (i - .5); color.coords[0] = (color.coords[0] ?? 0) + lightRotateStep * i; color.coords[1] = (color.coords[1] ?? 0) + lightSaturateStep * i; palette[step] = color.toString({ format: "hex" }); } palette[5] = baseHsl.clone().toString({ format: "hex" }); for (let i = 1; i <= darkStepsCount; i++) { const step = lightStepsCount + i; const color = baseHsl.clone(); color.coords[2] = (color.coords[2] ?? 0) - darknessStep * (i - .5); color.coords[0] = (color.coords[0] ?? 0) + darkRotateStep * i; color.coords[1] = (color.coords[1] ?? 0) + darkSaturateStep * i; palette[step] = color.toString({ format: "hex" }); } return palette; } /** * Generates an 11-color palette from a single base color, inspired by Mantine's color generation. * @returns {string[]} An array of 11 hex color strings. * @param {Color} baseColor - A `Color` instance for the base color. */ function gradientByMantine(baseColor) { const [h, s, l] = baseColor.to("hsl").coords.map((c) => c ?? 0); const palette = new Array(11); palette[5] = baseColor.toString({ format: "hex" }); for (let i = 0; i < 5; i++) { const factor = (5 - i) / 6; const newL = l + (100 - l) * factor; const newS = s - s * factor; palette[i] = new Color("hsl", [ h, newS, newL ]).toString({ format: "hex" }); } for (let i = 0; i < 5; i++) { const factor = (i + 1) / 6; const newL = l - l * factor; const newS = s + (100 - s) * factor; palette[i + 6] = new Color("hsl", [ h, newS, newL ]).toString({ format: "hex" }); } return palette; } /** * Generates an 11-shade color gradient from a single base color using different algorithms. * @param {string} baseHexColor - The base color in hexadecimal format (e.g., "#123456"). * @param {string} [mode='base'] - The gradient generation algorithm: 'base', 'saturation', or 'lightness'. * @returns {string[] | null} An array of 11 hex color strings, or `null` if the base color is invalid. */ function generateColorGradient(baseHexColor, mode = "steps") { let baseColor; try { baseColor = Color.get(baseHexColor); } catch (_e) { baseColor = Color.get(sample.navy); } switch (mode) { case "saturation": return gradientBySaturation(baseColor); case "lightness": return gradientByLightness(baseColor); case "mantine": return gradientByMantine(baseColor); case "chakra": return gradientByChakra(baseColor); default: return gradientBySteps(baseColor); } } //#endregion //#region src/ui/components/ColorPalette.ts var ColorPalette = class ColorPalette extends i$1 { constructor(..._args) { super(..._args); this.baseColor = "#228be6"; this.mode = "steps"; this.orientation = "horizontal"; this.value = ""; this.gradient = []; } static { this.styles = i$3` :host { display: flex; gap: var(--palette-gap, 4px); align-items: center; justify-content: center; } .swatch { width: var(--swatch-size, 22px); height: var(--swatch-size, 22px); border-radius: var(--swatch-radius, 4px); border: 1px solid var(--theme-border-color, #ccc); transition: transform 0.15s ease; cursor: pointer; background-clip: content-box; position: relative; display: flex; align-items: center; justify-content: center; box-sizing: border-box; } .swatch-inner { width: 100%; height: 100%; border-radius: var(--swatch-radius, 4px); background-color: var(--color); display: flex; align-items: center; justify-content: center; } .checkmark { position: absolute; top: 0; left: 0; right: 0; bottom: 0; opacity: 0; transition: opacity 0.15s ease; width: 100%; height: 100%; align-items: center; justify-content: center; display: flex; color: var(--text-color); font-weight: bold; font-size: 16px; line-height: 1; } .checkmark svg { width: 60%; height: 60%; } .swatch[checked] .checkmark { opacity: 1; } .swatch:hover { transform: scale(1.1); } `; } /** * Re-generates the color gradient whenever the `baseColor` or `mode` property changes. * @internal */ willUpdate(changedProperties) { if (changedProperties.has("baseColor") || changedProperties.has("mode")) this.gradient = generateColorGradient(this.baseColor, this.mode) ?? []; } handleSwatchClick(color) { this.value = color; this.dispatchEvent(new CustomEvent("input", { detail: { value: this.value }, bubbles: true, composed: true })); this.dispatchEvent(new CustomEvent("change", { detail: { value: this.value }, bubbles: true, composed: true })); } render() { return b$1` ${this.gradient.map((color) => { return b$1`
this.handleSwatchClick(color)} >
${IconCheck}
`; })} `; } }; __decorate([n$1({ type: String })], ColorPalette.prototype, "baseColor", void 0); __decorate([n$1({ type: String })], ColorPalette.prototype, "mode", void 0); __decorate([n$1({ type: String, reflect: true })], ColorPalette.prototype, "orientation", void 0); __decorate([n$1({ type: String })], ColorPalette.prototype, "selected", void 0); __decorate([n$1({ type: String, reflect: true })], ColorPalette.prototype, "value", void 0); __decorate([r$1()], ColorPalette.prototype, "gradient", void 0); ColorPalette = __decorate([t$1("color-palette")], ColorPalette); //#endregion //#region src/ui/components/ColorPanel.ts var ColorPanel = class ColorPanel extends i$1 { constructor(..._args) { super(..._args); this.value = ""; } static { this.styles = i$3` :host { display: flex; flex-direction: column; gap: 4px; width: 100%; } .SwatchGroup { display: grid; grid-template-columns: auto 1fr; align-items: center; column-gap: 8px; } .ColorName { font-size: 12px; color: var(--theme-text-color); text-transform: capitalize; min-width: 64px; } .Swatches { display: grid; grid-template-columns: repeat(9, 16px); gap: 8px; align-items: center; } .ThemeRadio { color: var(--mov-color-on-loud); height: 20px; width: 20px; border-radius: 3px; margin: 0; position: relative; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.2); } .ThemeRadio:hover, .ThemeRadio:focus-visible { outline: 2px solid var(--theme-border-color); outline-offset: 1px; } .ThemeRadio.selected { box-shadow: 0 0 0 2px var(--theme-body-background), 0 0 0 3px var(--theme-text-color); } .ThemeRadio svg { width: 10px; height: 10px; } .ThemeRadio.selected .icon-tabler-check { display: inline; } .ThemeRadio:not(.selected) .icon-tabler-check { display: none; } `; } /** * Handles the click event on a color swatch. * Updates the component's `value` property to the selected color's hex code. * Dispatches 'input' and 'change' custom events to notify parent components * about the color selection. * @param {MouseEvent} event The click event object. * @private */ handleColorClick(event) { this.value = event.currentTarget.title; this.dispatchEvent(new CustomEvent("input", { detail: { value: this.value }, bubbles: true, composed: true })); this.dispatchEvent(new CustomEvent("change", { detail: { value: this.value }, bubbles: true, composed: true })); } /** * Renders the grid of color swatches grouped by color family. * @internal */ render() { const swatchKeys = _.keys(colors).filter((k) => ![ "dark", "gray", "zinc", "neutral", "stone" ].includes(k)); const shades = [ 200, 300, 400, 500, 600, 700, 800, 900, 950 ]; return swatchKeys.map((key) => { return b$1`
${key}
${shades.map((shade) => { const hex = colors[key][shade]; const text = getTextColor(hex); return b$1` ${IconCheck} `; })}
`; }); } }; __decorate([n$1({ type: String, reflect: true })], ColorPanel.prototype, "value", void 0); __decorate([n$1({ type: String })], ColorPanel.prototype, "selected", void 0); ColorPanel = __decorate([t$1("color-panel")], ColorPanel); //#endregion //#region src/ui/components/ColorPicker.ts /** * An interactive color picker component that allows users to select a color. * * API is compatible with Web Awesome's wa-color-picker component (v3.3.1). * * @element mov-color-picker * @fires input - Dispatched continuously while the color is changing. * @fires change - Dispatched when the color selection is finalized. */ var ColorPicker = class ColorPicker extends i$1 { constructor(..._args) { super(..._args); this.value = "#228be6"; this.defaultValue = "#228be6"; this.label = ""; this.hint = ""; this.name = ""; this.disabled = false; this.size = "medium"; this.swatches = null; this.mode = "popup"; this.opened = false; this.popupDirection = "left"; this.sourceSpace = "srgb"; this.hsv = { h: 0, s: 0, v: 0 }; this.saturationThumbPosition = { x: 0, y: 0 }; this.hueThumbPosition = 0; this.isDraggingSaturation = false; this.isDraggingHue = false; } static { this.styles = i$3` :host { display: inline-block; position: relative; } .picker-container { width: 250px; box-sizing: border-box; } .picker-container.popup { position: absolute; top: calc(100% + 4px); left: 0; z-index: 10; border: 1px solid var(--theme-border-color); border-radius: 8px; background: var(--theme-background-color); padding: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .picker-container.popup.right { left: auto; right: 0; } .saturation-panel { position: relative; width: 100%; height: 180px; border-radius: 8px; cursor: crosshair; -webkit-tap-highlight-color: transparent; } .saturation-overlay-1, .saturation-overlay-2 { position: absolute; top: 0; left: 0; right: 0; bottom: 0; border-radius: inherit; } .saturation-overlay-1 { background: linear-gradient(to right, #fff, transparent); } .saturation-overlay-2 { background: linear-gradient(to top, #000, transparent); } .saturation-thumb { position: absolute; width: 16px; height: 16px; border: 2px solid #fff; border-radius: 50%; box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); transform: translate(-8px, -8px); pointer-events: none; } .sliders { display: flex; flex-direction: column; gap: 12px; margin-top: 12px; } .hue-slider { position: relative; width: 100%; height: 10px; border-radius: 5px; background: linear-gradient(to right, #f00, #ff0, #0f0, #0ff, #00f, #f0f, #f00); cursor: pointer; } .hue-thumb { position: absolute; top: 50%; width: 16px; height: 16px; border: 2px solid #fff; border-radius: 50%; box-shadow: 0 0 2px rgba(0, 0, 0, 0.5); transform: translate(-8px, -50%); pointer-events: none; } .swatches { display: grid; grid-template-columns: repeat(10, 1fr); gap: 8px; margin-top: 12px; } .swatch { width: 100%; aspect-ratio: 1; border-radius: 4px; border: 1px solid #dee2e6; cursor: pointer; padding: 0; display: flex; align-items: center; justify-content: center; box-sizing: border-box; transition: transform 0.1s; } .swatch:hover { transform: scale(1.1); } .popup-trigger { width: 96px; height: 32px; border-radius: 4px; border: 1px solid var(--theme-background-color); padding: 4px; box-sizing: border-box; cursor: pointer; background-color: var(--theme-hightlight-color); } .preview { width: 100%; height: 100%; border-radius: 2px; } `; } connectedCallback() { super.connectedCallback(); this.updateStateFromValue(this.value); window.addEventListener("mousemove", this.handleDrag.bind(this)); window.addEventListener("mouseup", this.handleDragEnd.bind(this)); window.addEventListener("touchmove", this.handleDrag.bind(this), { passive: false }); window.addEventListener("touchend", this.handleDragEnd.bind(this)); } disconnectedCallback() { super.disconnectedCallback(); window.removeEventListener("mousemove", this.handleDrag.bind(this)); window.removeEventListener("mouseup", this.handleDragEnd.bind(this)); window.removeEventListener("touchmove", this.handleDrag.bind(this)); window.removeEventListener("touchend", this.handleDragEnd.bind(this)); window.removeEventListener("click", this.handleClickOutside.bind(this)); } updated(changedProperties) { if (changedProperties.has("mode")) if (this.mode === "popup") window.addEventListener("click", this.handleClickOutside.bind(this)); else window.removeEventListener("click", this.handleClickOutside.bind(this)); } willUpdate(changedProperties) { if (changedProperties.has("value")) this.updateStateFromValue(this.value); if (changedProperties.has("mode") && this.mode === "inline") this.opened = false; } handleClickOutside(e) { if (this.opened && !e.composedPath().includes(this)) this.hide(); } show() { if (this.disabled || this.opened) return; this.opened = true; this.dispatchEvent(new CustomEvent("wa-show", { bubbles: true, composed: true })); setTimeout(() => { this.dispatchEvent(new CustomEvent("wa-after-show", { bubbles: true, composed: true })); }, 150); } hide() { if (!this.opened) return; this.opened = false; this.dispatchEvent(new CustomEvent("wa-hide", { bubbles: true, composed: true })); setTimeout(() => { this.dispatchEvent(new CustomEvent("wa-after-hide", { bubbles: true, composed: true })); }, 150); } togglePopup() { if (this.mode === "popup") if (this.opened) this.hide(); else { const triggerRect = this.getBoundingClientRect(); const pickerWidth = 250; let containerRect; const drawer = this.closest("mov-drawer"); if (drawer?.shadowRoot) { const dialog = drawer.shadowRoot.querySelector("dialog"); if (dialog) containerRect = dialog.getBoundingClientRect(); else containerRect = { left: 0, right: window.innerWidth }; } else containerRect = { left: 0, right: window.innerWidth }; if (triggerRect.left + pickerWidth > containerRect.right) if (triggerRect.right - pickerWidth > containerRect.left) this.popupDirection = "right"; else this.popupDirection = "left"; else this.popupDirection = "left"; this.show(); } } isSameColor(color1, color2) { if (!color1 || !color2) return false; return Color.deltaE(color1, color2, { method: "2000" }) < 1; } renderCheckIcon(color) { return b$1` `; } renderPickerBody() { const saturationPanelStyle = { backgroundColor: `hsl(${this.hsv.h}, 100%, 50%)` }; const hsv = { h: this.hsv.h, s: this.hsv.s * 100, v: this.hsv.v * 100 }; const saturationThumbStyle = { top: `${this.saturationThumbPosition.y}%`, left: `${this.saturationThumbPosition.x}%`, backgroundColor: new Color("hsv", [ hsv.h, hsv.s, hsv.v ]).toString({ format: "hex" }) }; const hueThumbStyle = { left: `${this.hueThumbPosition}%` }; return b$1`
${(this.swatches || _.entries(colors).filter(([name]) => ![ "dark", "gray", "zinc", "neutral", "stone" ].includes(name)).map(([, color]) => color["600"])).map((color) => b$1` `)}
`; } render() { const pickerClasses = { "picker-container": true, popup: this.mode === "popup", right: this.popupDirection === "right" }; const pickerBody = this.renderPickerBody(); if (this.mode === "popup") return b$1` ${this.opened ? b$1`
${pickerBody}
` : ""} `; return b$1`
${pickerBody}
`; } parseColor(color) { try { return Color.get(color); } catch (e) { console.error(`[mov-color-picker] Invalid color value: "${color}"`, e); return null; } } colorToHsv(color) { let [h, s, v] = color.to("srgb").to("hsv").coords.map((c) => c ?? 0); if (Number.isNaN(h)) { h = this.hsv.h || 0; s = 0; } s = Math.max(0, Math.min(100, s)) / 100; v = Math.max(0, Math.min(100, v)) / 100; return { h, s, v }; } updateStateFromValue(color) { const newColor = this.parseColor(color); if (!newColor) return; this.sourceSpace = newColor.space.id; const newHsv = this.colorToHsv(newColor); if (newHsv.h !== this.hsv.h || newHsv.s !== this.hsv.s || newHsv.v !== this.hsv.v) { this.hsv = newHsv; this.updateThumbPositions(); } } dispatchInput() { this.dispatchEvent(new CustomEvent("input", { detail: { value: this.value }, bubbles: true, composed: true })); } dispatchChange() { this.dispatchEvent(new CustomEvent("change", { detail: { value: this.value }, bubbles: true, composed: true })); } updateValueFromHsv() { const hsv = { h: this.hsv.h, s: this.hsv.s * 100, v: this.hsv.v * 100 }; const newColorFromHsv = new Color("hsv", [ hsv.h, hsv.s, hsv.v ]); let newValue; try { if (!this.sourceSpace || [ "srgb", "hsl", "hsv" ].includes(this.sourceSpace)) newValue = newColorFromHsv.to("srgb").toString({ format: "hex" }); else newValue = newColorFromHsv.to(this.sourceSpace).toString({ precision: 5 }); } catch (e) { console.error(`[mov-color-picker] Could not convert color to space ${this.sourceSpace}`, e); newValue = newColorFromHsv.to("srgb").toString({ format: "hex" }); } if (this.value !== newValue) { this.value = newValue; this.dispatchInput(); } } updateThumbPositions() { this.saturationThumbPosition = { x: this.hsv.s * 100, y: (1 - this.hsv.v) * 100 }; this.hueThumbPosition = this.hsv.h / 360 * 100; } /** * Initiates dragging for the saturation/value panel. * @param e The mouse or touch event. */ handleSaturationDragStart(e) { e.preventDefault(); this.isDraggingSaturation = true; this.saturationPanel = this.shadowRoot?.querySelector(".saturation-panel"); this.updateSaturation(e); } /** * Initiates dragging for the hue slider. * @param e The mouse or touch event. */ handleHueDragStart(e) { e.preventDefault(); this.isDraggingHue = true; this.hueSlider = this.shadowRoot?.querySelector(".hue-slider"); this.updateHue(e); } /** * Handles the continuous drag movement for both saturation and hue. * @param e The mouse or touch event. */ handleDrag(e) { if (this.isDraggingSaturation) this.updateSaturation(e); if (this.isDraggingHue) this.updateHue(e); } /** * Finalizes the drag operation and dispatches the 'change' event. */ handleDragEnd() { if (this.isDraggingSaturation || this.isDraggingHue) this.dispatchChange(); this.isDraggingSaturation = false; this.isDraggingHue = false; } getEventPosition(e) { if ("touches" in e) return { clientX: e.touches[0].clientX, clientY: e.touches[0].clientY }; return { clientX: e.clientX, clientY: e.clientY }; } updateSaturation(e) { if (!this.saturationPanel) return; const { clientX, clientY } = this.getEventPosition(e); const rect = this.saturationPanel.getBoundingClientRect(); const x = Math.max(0, Math.min(clientX - rect.left, rect.width)); const y = Math.max(0, Math.min(clientY - rect.top, rect.height)); this.hsv.s = x / rect.width; this.hsv.v = 1 - y / rect.height; this.updateValueFromHsv(); this.updateThumbPositions(); } updateHue(e) { if (!this.hueSlider) return; const { clientX } = this.getEventPosition(e); const rect = this.hueSlider.getBoundingClientRect(); const x = Math.max(0, Math.min(clientX - rect.left, rect.width)); this.hsv.h = x / rect.width * 360; this.updateValueFromHsv(); this.updateThumbPositions(); } selectSwatch(color) { this.value = color; this.dispatchInput(); this.dispatchChange(); } }; __decorate([n$1({ type: String })], ColorPicker.prototype, "value", void 0); __decorate([n$1({ type: String, attribute: "default-value" })], ColorPicker.prototype, "defaultValue", void 0); __decorate([n$1({ type: String })], ColorPicker.prototype, "label", void 0); __decorate([n$1({ type: String })], ColorPicker.prototype, "hint", void 0); __decorate([n$1({ type: String })], ColorPicker.prototype, "name", void 0); __decorate([n$1({ type: Boolean, reflect: true })], ColorPicker.prototype, "disabled", void 0); __decorate([n$1({ type: String, reflect: true })], ColorPicker.prototype, "size", void 0); __decorate([n$1({ type: Array })], ColorPicker.prototype, "swatches", void 0); __decorate([n$1({ type: String })], ColorPicker.prototype, "mode", void 0); __decorate([r$1()], ColorPicker.prototype, "opened", void 0); __decorate([r$1()], ColorPicker.prototype, "popupDirection", void 0); __decorate([r$1()], ColorPicker.prototype, "sourceSpace", void 0); __decorate([r$1()], ColorPicker.prototype, "hsv", void 0); __decorate([r$1()], ColorPicker.prototype, "saturationThumbPosition", void 0); __decorate([r$1()], ColorPicker.prototype, "hueThumbPosition", void 0); ColorPicker = __decorate([t$1("mov-color-picker")], ColorPicker); //#endregion //#region node_modules/lit-html/directives/choose.js /** * @license * Copyright 2021 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ var r = (r, o, t) => { for (const t of o) if (t[0] === r) return (0, t[1])(); return t?.(); }; //#endregion //#region src/ui/components/SegmentedControl.ts var SegmentedControl = class SegmentedControl extends i$1 { constructor(..._args) { super(..._args); this.value = ""; this.labelPosition = "side"; this.size = "medium"; this._options = []; this.resizeObserver = new ResizeObserver(() => this.updateThumbPosition()); } static { this.styles = i$3` :host { width: 100%; display: block; } .segmented-control { position: relative; display: flex; gap: 0.25rem; border-radius: 0.5rem; background-color: var(--theme-border-color); padding: 0.25rem; flex-wrap: wrap; } .thumb { position: absolute; top: 0; left: 0; border-radius: 0.5rem; background-color: var(--mov-color-fill-loud); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.3s cubic-bezier(0.4, 0, 0.2, 1), height 0.3s cubic-bezier(0.4, 0, 0.2, 1); z-index: 1; } .option { flex: 1; text-align: center; z-index: 2; /* Ensure button is above thumb */ position: relative; /* Needed to correctly position the button */ } .button { /* The button now acts as the interactive label */ width: 100%; display: flex; cursor: pointer; align-items: center; justify-content: center; border-radius: 0.5rem; border: none; /* Default colors when not selected */ color: var(--theme-text-color); background-color: transparent; transition: color 0.15s ease-in-out; flex-direction: row; gap: 0.25rem; padding: 0.5rem 0.75rem; /* Default padding (medium) */ font-size: 16px; /* Default font-size (medium) */ box-sizing: border-box; /* Include padding/border in element's total width/height */ } /* Selected State Styles - Driven by the 'selected' class */ .button.selected { color: var(--mov-color-on-loud); font-weight: 600; } /* Size Variations */ .button.small { padding: 0.25rem 0.5rem; font-size: 14px; } .button.large { padding: 0.75rem 1rem; font-size: 18px; } /* Label Position Variations */ .button.bottom { flex-direction: column; } .button.bottom.small { padding: 0.25rem; } .button.bottom.medium { padding: 0.5rem; } .button.bottom.large { padding: 0.75rem; } `; } connectedCallback() { super.connectedCallback(); this.resizeObserver.observe(this); } disconnectedCallback() { super.disconnectedCallback(); this.resizeObserver.unobserve(this); } /** * Handles the click event on an option button, updates the component's value, * and dispatches a `change` event. * @internal */ handleClick(_event, value) { this.value = value; this.dispatchEvent(new CustomEvent("change", { detail: this.value, bubbles: true, composed: true })); } /** * Gathers option data from the slotted `segmented-control-option` elements. * @internal */ handleSlotChange() { this._options = this._slotEl.assignedNodes({ flatten: true }).filter((node) => node.nodeName === "SEGMENTED-CONTROL-OPTION").map((node) => ({ value: node.getAttribute("value") ?? "", label: node.getAttribute("label") ?? "", icon: node.getAttribute("icon") ?? void 0 })); } firstUpdated() { this.handleSlotChange(); this.updateComplete.then(() => this.updateThumbPosition()); } updated(changedProperties) { super.updated(changedProperties); if (changedProperties.has("value") || changedProperties.has("_options") || changedProperties.has("labelPosition") || changedProperties.has("size")) Promise.resolve().then(() => this.updateThumbPosition()); } /** * Finds the currently selected option button and moves/resizes the thumb element. * @internal */ updateThumbPosition() { if (!this.thumb) return; const selectedButton = this.shadowRoot?.querySelector(".button.selected"); if (selectedButton) { const { offsetWidth, offsetHeight } = selectedButton; const buttonRect = selectedButton.getBoundingClientRect(); const containerRect = this.shadowRoot?.querySelector(".segmented-control")?.getBoundingClientRect(); const offsetX = buttonRect.left - (containerRect?.left ?? 0); const offsetY = buttonRect.top - (containerRect?.top ?? 0); this.thumb.style.transform = `translate(${offsetX}px, ${offsetY}px)`; this.thumb.style.width = `${offsetWidth}px`; this.thumb.style.height = `${offsetHeight}px`; } else { this.thumb.style.width = "0px"; this.thumb.style.height = "0px"; } } render() { return b$1`
${this._options.map((option) => b$1`
`)}
`; } }; __decorate([n$1({ type: String, reflect: true })], SegmentedControl.prototype, "value", void 0); __decorate([n$1({ type: String })], SegmentedControl.prototype, "labelPosition", void 0); __decorate([n$1({ type: String })], SegmentedControl.prototype, "size", void 0); __decorate([r$1()], SegmentedControl.prototype, "_options", void 0); __decorate([e$2(".thumb")], SegmentedControl.prototype, "thumb", void 0); __decorate([e$2("slot")], SegmentedControl.prototype, "_slotEl", void 0); SegmentedControl = __decorate([t$1("segmented-control")], SegmentedControl); var SegmentedControlOption = class SegmentedControlOption extends i$1 { constructor(..._args2) { super(..._args2); this.value = ""; this.label = ""; } /** * This component is a data container and does not need its own shadow DOM. * @internal */ createRenderRoot() { return this; } }; __decorate([n$1({ type: String, reflect: true })], SegmentedControlOption.prototype, "value", void 0); __decorate([n$1({ type: String, reflect: true })], SegmentedControlOption.prototype, "label", void 0); __decorate([n$1({ type: String, reflect: true })], SegmentedControlOption.prototype, "icon", void 0); SegmentedControlOption = __decorate([t$1("segmented-control-option")], SegmentedControlOption); //#endregion //#region src/ui/components/Switch.ts var Switch = class Switch extends i$1 { constructor(..._args) { super(..._args); this.name = ""; this.value = "on"; this.checked = false; this.defaultChecked = false; this.disabled = false; this.required = false; this.size = "medium"; this.hint = ""; this.design = "graphical"; this.textOn = "ON"; this.textOff = "OFF"; } static { this.styles = i$3` :host { --switch-width: 3rem; --switch-height: 1.5rem; --knob-size: 1.25rem; display: inline-block; } :host([size='small']) { --switch-width: 2.5rem; --switch-height: 1.25rem; --knob-size: 16px; } :host([size='large']) { --switch-width: 4rem; --switch-height: 2rem; --knob-size: 1.75rem; } *, *::before, *::after { box-sizing: border-box; } input { display: none; } .switch { display: inline-block; position: relative; width: var(--switch-width); height: var(--switch-height); border-radius: var(--switch-height); background-color: #d7062a; border: 1px solid #d7062a; transition: background-color 0.3s, border-color 0.3s; cursor: pointer; } input:checked + .switch { background-color: #50ac5d; border-color: #50ac5d; } .switch.textual { background-color: var(--mov-color-on-loud); border-color: var(--mov-color-on-loud); } input:checked + .switch.textual { background-color: var(--mov-color-fill-loud); border-color: var(--mov-color-fill-loud); } input:disabled + .switch { background-color: #eee; border-color: #ccc; cursor: not-allowed; } .knob { position: absolute; top: 1px; left: 1px; width: var(--knob-size); height: var(--knob-size); background-color: #fff; border-radius: 50%; box-shadow: 0 0 3px rgba(0, 0, 0, 0.3); transition: left 0.3s; display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: bold; font-family: Arial; color: #333; } input:checked + .switch .knob { left: calc(100% - var(--knob-size) - 1px); } .switch:focus { outline: 2px solid #0a6ed1; outline-offset: 2px; } .icon { width: 16px; height: 16px; fill: none; } .text { font-size: 12px; font-weight: bold; color: #333; } .hint { font-size: 13px; opacity: 0.7; margin-top: 0.25rem; } `; } handleChange(event) { if (!this.disabled) { this.checked = event.target.checked; this.dispatchEvent(new CustomEvent("change", { detail: { checked: this.checked }, bubbles: true, composed: true })); this.dispatchEvent(new CustomEvent("input", { detail: { checked: this.checked }, bubbles: true, composed: true })); } } render() { const design = this.design.toLowerCase(); let knobContent; if (design === "graphical") knobContent = b$1`${this.checked ? IconCheck : IconX}`; else knobContent = b$1`${this.checked ? this.textOn : this.textOff}`; return b$1`
${this.hint}
`; } }; __decorate([n$1({ type: String })], Switch.prototype, "name", void 0); __decorate([n$1({ type: String })], Switch.prototype, "value", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Switch.prototype, "checked", void 0); __decorate([n$1({ type: Boolean, reflect: true, attribute: "default-checked" })], Switch.prototype, "defaultChecked", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Switch.prototype, "disabled", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Switch.prototype, "required", void 0); __decorate([n$1({ type: String, reflect: true })], Switch.prototype, "size", void 0); __decorate([n$1({ type: String })], Switch.prototype, "hint", void 0); __decorate([n$1({ type: String, reflect: true })], Switch.prototype, "design", void 0); __decorate([n$1({ type: String })], Switch.prototype, "textOn", void 0); __decorate([n$1({ type: String })], Switch.prototype, "textOff", void 0); Switch = __decorate([t$1("mov-switch")], Switch); //#endregion //#region src/ui/styles/slider.css?inline var slider_default = ":host {\r\n display: block;\r\n --mov-slider-track-height: 6px;\r\n --mov-slider-thumb-size: 18px;\r\n --mov-slider-tooltip-offset: 24px;\r\n -webkit-user-select: none;\r\n user-select: none;\r\n touch-action: none;\r\n width: 100%;\r\n}\r\n\r\n:host([vertical]) {\r\n display: inline-block;\r\n}\r\n\r\n/* Container */\r\n.mov-slider__container {\r\n position: relative;\r\n display: flex;\r\n align-items: center;\r\n min-height: 24px;\r\n}\r\n\r\n:host([vertical]) .mov-slider__container {\r\n flex-direction: column;\r\n min-height: auto;\r\n min-width: 24px;\r\n}\r\n\r\n/* Track */\r\n.mov-slider__track {\r\n position: relative;\r\n flex: 1;\r\n height: var(--mov-slider-track-height);\r\n background: var(--theme-border-color, #ccc);\r\n border-radius: 9999px;\r\n cursor: pointer;\r\n}\r\n\r\n:host([vertical]) .mov-slider__track {\r\n width: var(--mov-slider-track-height);\r\n height: 100%;\r\n min-height: 8rem;\r\n}\r\n\r\n/* Progress */\r\n.mov-slider__progress {\r\n position: absolute;\r\n height: 100%;\r\n background: var(--mov-color-fill-loud);\r\n border-radius: 9999px;\r\n pointer-events: none;\r\n transition: background-color 150ms;\r\n}\r\n\r\n.mov-slider__progress--min-gap {\r\n background: #f59e0b; /* warning color */\r\n animation: pulse-gap 2s ease-in-out infinite;\r\n}\r\n\r\n@keyframes pulse-gap {\r\n 0%,\r\n 100% {\r\n opacity: 1;\r\n }\r\n 50% {\r\n opacity: 0.7;\r\n }\r\n}\r\n\r\n:host([vertical]) .mov-slider__progress {\r\n width: 100%;\r\n height: auto;\r\n}\r\n\r\n/* Thumb */\r\n.mov-slider__thumb {\r\n position: absolute;\r\n width: var(--mov-slider-thumb-size);\r\n height: var(--mov-slider-thumb-size);\r\n background: white;\r\n border: 3px solid var(--mov-color-fill-loud);\r\n border-radius: 9999px;\r\n cursor: grab;\r\n transform: translate(-50%, -50%);\r\n top: 50%;\r\n transition:\r\n transform 150ms,\r\n border-color 150ms,\r\n box-shadow 150ms;\r\n box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\r\n touch-action: none;\r\n box-sizing: border-box;\r\n}\r\n\r\n.mov-slider__thumb:active {\r\n cursor: grabbing;\r\n transform: translate(-50%, -50%) scale(1.1);\r\n}\r\n\r\n:host([vertical]) .mov-slider__thumb {\r\n transform: translate(-50%, 50%);\r\n inset-inline-start: 50%;\r\n top: auto;\r\n}\r\n\r\n:host([vertical]) .mov-slider__thumb:active {\r\n transform: translate(-50%, 50%) scale(1.1);\r\n}\r\n\r\n.mov-slider__thumb:hover {\r\n border-color: var(--mov-color-fill-loud);\r\n box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);\r\n}\r\n\r\n/* Visual focus state */\r\n.mov-slider__thumb--focused {\r\n outline: 3px solid var(--mov-color-fill-loud);\r\n outline-offset: 2px;\r\n}\r\n\r\n/* Active drag state */\r\n.mov-slider__thumb--active {\r\n transform: translate(-50%, -50%) scale(1.1);\r\n z-index: 1;\r\n}\r\n\r\n:host([vertical]) .mov-slider__thumb--active {\r\n transform: translate(-50%, 50%) scale(1.1);\r\n}\r\n\r\n/* Readonly state */\r\n:host([readonly]) .mov-slider__thumb {\r\n cursor: default;\r\n border-color: var(--theme-border-color);\r\n}\r\n\r\n:host([readonly]) .mov-slider__thumb:active {\r\n transform: translate(-50%, -50%);\r\n}\r\n\r\n/* Input (hidden but accessible) */\r\n.mov-slider__input {\r\n position: absolute;\r\n opacity: 0;\r\n pointer-events: none;\r\n}\r\n\r\n/* Tooltip */\r\n.mov-slider__tooltip {\r\n position: absolute;\r\n bottom: var(--mov-slider-tooltip-offset);\r\n inset-inline-start: 50%;\r\n transform: translateX(-50%);\r\n background: var(--theme-hightlight-color, #333);\r\n color: white;\r\n padding: 2px 8px;\r\n border-radius: 4px;\r\n font-size: 14px;\r\n white-space: nowrap;\r\n pointer-events: none;\r\n opacity: 0;\r\n transition: opacity 150ms;\r\n z-index: 2;\r\n}\r\n\r\n.mov-slider__thumb:hover .mov-slider__tooltip,\r\n.mov-slider__thumb--focused .mov-slider__tooltip,\r\n.mov-slider__thumb--active .mov-slider__tooltip {\r\n opacity: 1;\r\n}\r\n\r\n:host([vertical]) .mov-slider__tooltip {\r\n bottom: auto;\r\n inset-inline-start: var(--mov-slider-tooltip-offset);\r\n top: 50%;\r\n transform: translateY(-50%);\r\n}\r\n\r\n/* Ticks */\r\n.mov-slider__ticks {\r\n position: absolute;\r\n top: 50%;\r\n inset-inline: 0;\r\n height: 8px;\r\n pointer-events: none;\r\n}\r\n\r\n:host([vertical]) .mov-slider__ticks {\r\n top: 0;\r\n bottom: 0;\r\n inset-inline-start: 50%;\r\n width: 8px;\r\n height: auto;\r\n}\r\n\r\n.mov-slider__tick {\r\n position: absolute;\r\n width: 1px;\r\n height: 8px;\r\n background: var(--theme-border-color);\r\n transform: translateX(-50%);\r\n}\r\n\r\n:host([vertical]) .mov-slider__tick {\r\n width: 8px;\r\n height: 1px;\r\n transform: translateY(-50%);\r\n}\r\n\r\n.mov-slider__tick-label {\r\n position: absolute;\r\n top: 12px;\r\n font-size: 12px;\r\n color: var(--theme-text-color);\r\n opacity: 0.7;\r\n transform: translateX(-50%);\r\n white-space: nowrap;\r\n}\r\n\r\n:host([vertical]) .mov-slider__tick-label {\r\n top: auto;\r\n inset-inline-start: 12px;\r\n transform: translateY(-50%);\r\n}\r\n\r\n/* Sizes */\r\n:host([size=\"small\"]) {\r\n --mov-slider-track-height: 4px;\r\n --mov-slider-thumb-size: 14px;\r\n}\r\n\r\n:host([size=\"large\"]) {\r\n --mov-slider-track-height: 10px;\r\n --mov-slider-thumb-size: 22px;\r\n}\r\n\r\n/* States */\r\n:host([disabled]) {\r\n opacity: 0.6;\r\n pointer-events: none;\r\n}\r\n\r\n:host([disabled]) .mov-slider__thumb {\r\n cursor: not-allowed;\r\n border-color: var(--theme-border-color);\r\n background: #f3f4f6;\r\n}\r\n\r\n:host([invalid]) .mov-slider__progress {\r\n background: #ef4444; /* danger color */\r\n}\r\n\r\n/* Help & Error Text */\r\n.mov-form-control__label {\r\n display: block;\r\n margin-bottom: 0.5rem;\r\n color: var(--theme-text-color);\r\n}\r\n\r\n.mov-form-control__helper,\r\n.mov-form-control__error {\r\n margin-top: 0.5rem;\r\n font-size: 14px;\r\n}\r\n\r\n.mov-form-control__helper {\r\n color: var(--theme-text-color);\r\n opacity: 0.8;\r\n}\r\n\r\n.mov-form-control__error {\r\n color: #ef4444;\r\n}\r\n\r\n.mov-slider__live-region {\r\n position: absolute;\r\n overflow: hidden;\r\n clip: rect(0 0 0 0);\r\n height: 1px;\r\n width: 1px;\r\n margin: -1px;\r\n padding: 0;\r\n border: 0;\r\n}\r\n:host([show-ticks]) {\r\n padding-bottom: 15px;\r\n margin-left: 5px;\r\n margin-right: 5px;\r\n}\r\n\r\n/* Filled variant */\r\n:host([filled]) .mov-slider__thumb {\r\n background: var(--mov-color-fill-loud);\r\n border-color: var(--mov-color-fill-loud);\r\n}\r\n"; //#endregion //#region src/ui/components/Slider.ts var Slider = class Slider extends i$1 { static { this.styles = [r$4(slider_default)]; } constructor() { super(); this.label = ""; this.helpText = ""; this.errorMessage = ""; this.min = 0; this.max = 100; this.step = 1; this.value = 0; this.dual = false; this.vertical = false; this.filled = false; this.size = "medium"; this.disabled = false; this.readonly = false; this.invalid = false; this.showTooltip = false; this.showTicks = false; this.tickStep = 0; this.tickCount = 0; this.focusedThumb = null; this.draggingThumb = null; this.activeDrag = null; this._handlePointerMove = this._handlePointerMove.bind(this); this._handlePointerUp = this._handlePointerUp.bind(this); } get values() { if (Array.isArray(this.value)) return this.value; return [this.min, this.value]; } getPercentage(val) { return (val - this.min) / (this.max - this.min) * 100; } getValueFromPercentage(percentage) { const rawValue = this.min + percentage / 100 * (this.max - this.min); return this.step ? Math.round(rawValue / this.step) * this.step : rawValue; } getValueFromPointer(clientX, clientY, trackRect) { let percentage; if (this.vertical) percentage = (trackRect.bottom - clientY) / trackRect.height * 100; else percentage = (clientX - trackRect.left) / trackRect.width * 100; percentage = Math.max(0, Math.min(100, percentage)); return this.getValueFromPercentage(percentage); } clampValue(val) { let clamped = Math.max(this.min, Math.min(this.max, val)); if (this.step) clamped = Math.round(clamped / this.step) * this.step; return Number(clamped.toFixed(10)); } handleThumbPointerDown(e, thumbType) { if (this.disabled || this.readonly || !this.track) return; e.preventDefault(); e.stopPropagation(); e.currentTarget.setPointerCapture(e.pointerId); this.activeDrag = { thumb: thumbType, trackRect: this.track.getBoundingClientRect() }; this.draggingThumb = thumbType; document.addEventListener("pointermove", this._handlePointerMove); document.addEventListener("pointerup", this._handlePointerUp); document.addEventListener("pointercancel", this._handlePointerUp); } _handlePointerMove(e) { if (!this.activeDrag || this.disabled || this.readonly) return; const { thumb, trackRect } = this.activeDrag; const newValue = this.getValueFromPointer(e.clientX, e.clientY, trackRect); if (this.dual) { const currentValues = this.values; if (thumb === "min") { const clampedValue = Math.min(newValue, currentValues[1] - (this.step || 1)); this.updateValue([clampedValue, currentValues[1]], "input"); } else { const clampedValue = Math.max(newValue, currentValues[0] + (this.step || 1)); this.updateValue([currentValues[0], clampedValue], "input"); } } else this.updateValue(newValue, "input"); } _handlePointerUp(e) { if (!this.activeDrag) return; (this.shadowRoot?.querySelector(".mov-slider__thumb--active"))?.releasePointerCapture(e.pointerId); this.updateValue(this.value, "change"); this.activeDrag = null; this.draggingThumb = null; document.removeEventListener("pointermove", this._handlePointerMove); document.removeEventListener("pointerup", this._handlePointerUp); document.removeEventListener("pointercancel", this._handlePointerUp); } updateValue(newValue, type = "change") { if (this.readonly || this.disabled) return; if (this.dual && Array.isArray(newValue)) { newValue = [this.clampValue(newValue[0]), this.clampValue(newValue[1])]; if (newValue[0] > newValue[1]) newValue = [newValue[1], newValue[0]]; } else if (!this.dual && typeof newValue === "number") newValue = this.clampValue(newValue); this.value = newValue; this.dispatchEvent(new CustomEvent(type, { bubbles: true, composed: true, detail: { value: newValue } })); } handleTrackClick(e) { if (this.disabled || this.readonly || !this.track) return; const rect = this.track.getBoundingClientRect(); const newValue = this.getValueFromPointer(e.clientX, e.clientY, rect); if (this.dual) { const currentValues = this.values; if (Math.abs(newValue - currentValues[0]) < Math.abs(newValue - currentValues[1])) this.updateValue([newValue, currentValues[1]]); else this.updateValue([currentValues[0], newValue]); } else this.updateValue(newValue); } renderTicks() { if (!this.showTicks) return null; const tickValues = /* @__PURE__ */ new Set(); tickValues.add(this.min); tickValues.add(this.max); let step = this.tickStep; if (this.tickCount > 1) step = Math.round((this.max - this.min) / (this.tickCount - 1)); if (step > 0) { const tickCount = Math.floor((this.max - this.min) / step); if (tickCount <= 100) for (let i = 1; i <= tickCount; i++) { const val = this.min + i * step; if (val < this.max) tickValues.add(Number(val.toFixed(10))); } } return b$1`
${Array.from(tickValues).sort((a, b) => a - b).map((val) => { const percentage = this.getPercentage(val); return b$1`
${val}
`; })}
`; } renderThumb(val, thumbType) { const percentage = this.getPercentage(val); const isFocused = this.focusedThumb === thumbType; const isDragging = this.draggingThumb === thumbType; const style = this.vertical ? `bottom: ${percentage}%` : `inset-inline-start: ${percentage}%`; return b$1`
this.handleThumbPointerDown(e, thumbType)} > ${this.showTooltip ? b$1`
${val}
` : ""}
`; } renderProgress() { const vals = this.values; if (this.dual) { const start = this.getPercentage(vals[0]); const end = this.getPercentage(vals[1]); return b$1`
`; } const end = this.getPercentage(vals[1]); return b$1`
`; } render() { const vals = this.values; return b$1`
${this.label ? b$1`` : ""}
${this.renderProgress()} ${this.renderTicks()} ${this.dual ? b$1`${this.renderThumb(vals[0], "min")}${this.renderThumb(vals[1], "max")}` : this.renderThumb(vals[1], "single")}
${this.helpText && !this.invalid ? b$1`
${this.helpText}
` : ""} ${this.invalid && this.errorMessage ? b$1`
${this.errorMessage}
` : ""}
`; } }; __decorate([n$1({ type: String })], Slider.prototype, "label", void 0); __decorate([n$1({ attribute: "help-text" })], Slider.prototype, "helpText", void 0); __decorate([n$1({ attribute: "error-message" })], Slider.prototype, "errorMessage", void 0); __decorate([n$1({ type: Number })], Slider.prototype, "min", void 0); __decorate([n$1({ type: Number })], Slider.prototype, "max", void 0); __decorate([n$1({ type: Number })], Slider.prototype, "step", void 0); __decorate([n$1({ type: Object })], Slider.prototype, "value", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Slider.prototype, "dual", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Slider.prototype, "vertical", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Slider.prototype, "filled", void 0); __decorate([n$1({ reflect: true })], Slider.prototype, "size", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Slider.prototype, "disabled", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Slider.prototype, "readonly", void 0); __decorate([n$1({ type: Boolean, reflect: true })], Slider.prototype, "invalid", void 0); __decorate([n$1({ type: Boolean, attribute: "show-tooltip" })], Slider.prototype, "showTooltip", void 0); __decorate([n$1({ type: Boolean, attribute: "show-ticks" })], Slider.prototype, "showTicks", void 0); __decorate([n$1({ type: Number, attribute: "tick-step" })], Slider.prototype, "tickStep", void 0); __decorate([n$1({ type: Number, attribute: "tick-count" })], Slider.prototype, "tickCount", void 0); __decorate([r$1()], Slider.prototype, "focusedThumb", void 0); __decorate([r$1()], Slider.prototype, "draggingThumb", void 0); __decorate([e$2(".mov-slider__track")], Slider.prototype, "track", void 0); Slider = __decorate([t$1("mov-slider")], Slider); //#endregion //#region node_modules/@braintree/sanitize-url/dist/constants.js var require_constants = /* @__PURE__ */ __commonJSMin(((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.BLANK_URL = exports.relativeFirstCharacters = exports.whitespaceEscapeCharsRegex = exports.urlSchemeRegex = exports.ctrlCharactersRegex = exports.htmlCtrlEntityRegex = exports.htmlEntitiesRegex = exports.invalidProtocolRegex = void 0; exports.invalidProtocolRegex = /^([^\w]*)(javascript|data|vbscript)/im; exports.htmlEntitiesRegex = /&#(\w+)(^\w|;)?/g; exports.htmlCtrlEntityRegex = /&(newline|tab);/gi; exports.ctrlCharactersRegex = /[\u0000-\u001F\u007F-\u009F\u2000-\u200D\uFEFF]/gim; exports.urlSchemeRegex = /^.+(:|:)/gim; exports.whitespaceEscapeCharsRegex = /(\\|%5[cC])((%(6[eE]|72|74))|[nrt])/g; exports.relativeFirstCharacters = [".", "/"]; exports.BLANK_URL = "about:blank"; })); //#endregion //#region src/core/download.ts var import_dist = (/* @__PURE__ */ __commonJSMin(((exports) => { Object.defineProperty(exports, "__esModule", { value: true }); exports.sanitizeUrl = sanitizeUrl; var constants_1 = require_constants(); function isRelativeUrlWithoutProtocol(url) { return constants_1.relativeFirstCharacters.indexOf(url[0]) > -1; } function decodeHtmlCharacters(str) { return str.replace(constants_1.ctrlCharactersRegex, "").replace(constants_1.htmlEntitiesRegex, function(match, dec) { return String.fromCharCode(dec); }); } function isValidUrl(url) { return URL.canParse(url); } function decodeURI(uri) { try { return decodeURIComponent(uri); } catch (e) { return uri; } } function sanitizeUrl(url) { if (!url) return constants_1.BLANK_URL; var charsToDecode; var decodedUrl = decodeURI(url.trim()); do { decodedUrl = decodeHtmlCharacters(decodedUrl).replace(constants_1.htmlCtrlEntityRegex, "").replace(constants_1.ctrlCharactersRegex, "").replace(constants_1.whitespaceEscapeCharsRegex, "").trim(); decodedUrl = decodeURI(decodedUrl); charsToDecode = decodedUrl.match(constants_1.ctrlCharactersRegex) || decodedUrl.match(constants_1.htmlEntitiesRegex) || decodedUrl.match(constants_1.htmlCtrlEntityRegex) || decodedUrl.match(constants_1.whitespaceEscapeCharsRegex); } while (charsToDecode && charsToDecode.length > 0); var sanitizedUrl = decodedUrl; if (!sanitizedUrl) return constants_1.BLANK_URL; if (isRelativeUrlWithoutProtocol(sanitizedUrl)) return sanitizedUrl; var trimmedUrl = sanitizedUrl.trimStart(); var urlSchemeParseResults = trimmedUrl.match(constants_1.urlSchemeRegex); if (!urlSchemeParseResults) return sanitizedUrl; var urlScheme = urlSchemeParseResults[0].toLowerCase().trim(); if (constants_1.invalidProtocolRegex.test(urlScheme)) return constants_1.BLANK_URL; var backSanitized = trimmedUrl.replace(/\\/g, "/"); if (urlScheme === "mailto:" || urlScheme.includes("://")) return backSanitized; if (urlScheme === "http:" || urlScheme === "https:") { if (!isValidUrl(backSanitized)) return constants_1.BLANK_URL; var url_1 = new URL(backSanitized); url_1.protocol = url_1.protocol.toLowerCase(); url_1.hostname = url_1.hostname.toLowerCase(); return url_1.toString(); } return backSanitized; } })))(); /** * Gets the file extension from a MIME type. * @param {string} [mime] - The MIME type of the file. * @returns {string} The corresponding file extension (e.g., 'jpg', 'png'). Defaults to 'png'. */ function extFromMime(mime) { switch (mime) { case "image/jpeg": return "jpg"; case "image/png": return "png"; case "image/webp": return "webp"; case "image/gif": return "gif"; case "image/bmp": return "bmp"; default: return "png"; } } /** * Fetches an image from its URL and returns it as a Blob. * @param page */ async function getBlobFromFetch(page) { if (!page.src) return null; try { const response = await fetch(page.src); if (response.ok) { logScript(`Got blob for page ${page.src} from fetch`); return await response.blob(); } } catch (error) { logScript(`Failed to get blob for page ${page.src} from fetch`, error); } if (typeof GM_xmlhttpRequest !== "undefined") return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: page.src, responseType: "blob", onload: (response) => { if (response.status === 200) { logScript(`Got blob for page ${page.src} from GM_xmlhttpRequest`); resolve(response.response); } else { logScript(`Failed to get blob for page ${page.src} from GM_xmlhttpRequest`, response.statusText); resolve(null); } }, onerror: (error) => { logScript(`Failed to get blob for page ${page.src} from GM_xmlhttpRequest`, error); resolve(null); } }); }); return null; } /** * Draws an image onto a canvas and returns it as a Blob. * @param page */ async function getBlobFromCanvas(page) { const img = page.ref?.value; if (!img) return null; try { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (ctx) { canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; ctx.drawImage(img, 0, 0); return await new Promise((resolve) => { canvas.toBlob((blob) => { if (blob) logScript(`Got blob for page ${page.src} from canvas`); resolve(blob); }, "image/png", 1); }); } } catch (e) { logScript(`Failed to get blob for page ${page.src} from canvas`, e); } return null; } /** * Tries to get the image blob from cache, fetch or canvas. * @param page */ async function getImageBlob(page) { if (page.blob) { logScript(`Got blob for page ${page.src} from cache`); return page.blob; } const blob = await getBlobFromFetch(page) || await getBlobFromCanvas(page); if (!blob) logScript(`Failed to get blob for page ${page.src}`); return blob; } /** * Generates a zip file from the images stored in the application state and initiates a download. * The zip file is named after the manga title. * @returns {Promise} A promise that resolves when the zip generation and download process is initiated. */ async function generateZip() { setAppStateValue("download", "working"); const zip = new JSZip(); const images = getAppStateValue("images") ?? {}; const manga = getAppStateValue("manga"); const total = manga?.pages ?? 0; const digits = Math.floor(Math.log10(total || 1)) + 1; const imageEntries = _.sortBy(_.entries(images), ([key]) => Number(key)); const failedDownloads = []; const updateProgress = (current) => { setAppStateValue("dialog", { open: true, title: getLocaleString("BUTTON_DOWNLOAD"), content: b$1`

${getLocaleString("DOWNLOAD_PROGRESS").replace("##num##", current.toString()).replace("##total##", total.toString())}

`, footer: b$1` setAppStateValue("download", "cancelled")}> ${getLocaleString("CANCEL")} ` }); }; updateProgress(0); let count = 0; for (const [key, page] of imageEntries) { if (getAppStateValue("download") === "cancelled") { logScript("Download cancelled"); setAppStateValue("dialog", null); setAppStateValue("download", void 0); return; } try { const blob = await getImageBlob(page); if (blob) { const ext = extFromMime(blob.type); const name = `Page-${Number(key).toString().padStart(digits, "0")}.${ext}`; logScript(`${name} Added to Zip from Blob`); zip.file(name, blob, { createFolders: true, compression: "DEFLATE" }); } else failedDownloads.push(page.src ?? key); } catch (error) { logScript(`Error processing page ${key}`, error); failedDownloads.push(page.src ?? key); } finally { count += 1; updateProgress(count); } } setAppStateValue("dialog", { open: true, title: getLocaleString("BUTTON_DOWNLOAD"), content: b$1`

${getLocaleString("GENERATING_ZIP")}

`, footer: b$1`` }); if (failedDownloads.length > 0) { logScript("Some images failed to download:", failedDownloads); zip.file("failed_pages.txt", failedDownloads.join("\n")); } logScript("Generating Zip"); zip.generateAsync({ type: "blob" }).then((content) => { logScript("Download Ready"); const zipName = `${manga?.title ?? document.title}.zip`; window.saveAs(content, zipName, { autoBom: false }); if (failedDownloads.length > 0) setAppStateValue("dialog", { open: true, title: getLocaleString("DOWNLOAD_INCOMPLETE"), icon: "warning", content: b$1`

${getLocaleString("DOWNLOAD_INCOMPLETE_MESSAGE")}

`, footer: b$1` setAppStateValue("dialog", null)}> ${getLocaleString("CLOSE")} ` }); else setAppStateValue("dialog", null); }).catch((err) => { logScript("Error generating zip", err); setAppStateValue("dialog", { open: true, title: getLocaleString("WARNING"), icon: "error", content: b$1`

Error generating zip: ${err.message}

`, footer: b$1` setAppStateValue("dialog", null)}> ${getLocaleString("CLOSE")} ` }); }).finally(() => { setAppStateValue("download", void 0); }); } //#endregion //#region src/ui/events/globals.ts /** * Event handler to initiate the download of the chapter as a ZIP file. * It checks if a download is already in progress to prevent multiple downloads. */ function buttonStartDownload() { if (getAppStateValue("download") === "working") return; logScript("Downloading Chapter"); generateZip().catch((err) => logScript("Error downloading chapter", err)); } /** * Event handler to toggle the visibility of the individual page controls globally. */ function buttonGlobalHideImageControls() { changeSettingsValue("hidePageControls", (b) => !b); } /** * Event handler for clickable elements that should redirect the browser. * It handles normal clicks, as well as Ctrl+Click and middle-mouse clicks for opening in a new tab. * It also includes a special case for the 'series' link to navigate back in history. * @param {Event} event - The click event. */ function buttonRedirectURL(event) { const element = event.currentTarget || event.target; const url = element.getAttribute("value") ?? element.getAttribute("href"); if (event.button !== 1 && !event.ctrlKey) { if (url && url !== "#") window.location.href = (0, import_dist.sanitizeUrl)(url); else if (element.id === "series") if (isStandalone()) window.location.href = window.location.pathname; else window.history.back(); } } //#endregion //#region src/ui/events/navigation.ts /** * Scrolls the view to a specific element. * The scrolling behavior adapts based on the current view mode: * - In 'Fluid' modes, it scrolls the internal `#Chapter` container. * - In other modes, it scrolls the main window. * @param {HTMLElement | undefined | null} ele - The element to scroll into view. */ function scrollToElement(ele) { if (!ele) return; if (getSettingsValue("viewMode").startsWith("Fluid")) { const container = getAppStateValue("chapter").value; if (container) { const rect = ele.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); container.scrollBy({ left: rect.left - containerRect.left, top: rect.top - containerRect.top, behavior: "instant" }); } } else { const rect = ele.getBoundingClientRect(); window.scrollTo({ top: rect.top + window.scrollY, left: rect.left + window.scrollX, behavior: "instant" }); } } /** * Listens for changes to the `scrollToPage` app state value. * When `scrollToPage` is set (and not undefined initially), it scrolls to the specified page. */ appState.listen((value, _oldValue, changedKey) => { if (changedKey === "scrollToPage" && value.scrollToPage !== void 0) { if (value.scrollToPage <= 0) window.scrollTo(0, 0); else scrollToElement(getAppStateValue("images")?.[value.scrollToPage]?.ref?.value); setTimeout(() => setAppStateValue("scrollToPage", void 0), 10); } }); /** * Event handler for the "Go to Page" dropdown selector. * It reads the selected page number and scrolls to the corresponding page element. * @param {Event} event - The change event from the ``); } render() { return b$1`

${getLocaleString("KEYBINDINGS")}

${getAppStateValue("panel") === "keybindingsEditor" ? b$1` saveKeybindings(this.keybindsRefs)} > ${getLocaleString("BUTTON_SAVE")} ` : b$1` ${getLocaleString("BUTTON_EDIT")} `}
${getAppStateValue("panel") === "keybindingsEditor" ? this.keybindEditor() : this.keybindList()}
${o$5(getLocaleString("KEYBIND_RULES"))}
`; } }; KeybindingsPanel = __decorate([t$1("keybindings-panel"), (0, import_lib.useStores)(settings, locale, appState)], KeybindingsPanel); //#endregion //#region node_modules/lit-html/directives/map.js /** * @license * Copyright 2021 Google LLC * SPDX-License-Identifier: BSD-3-Clause */ function* o(o, f) { if (void 0 !== o) { let i = 0; for (const t of o) yield f(t, i++); } } //#endregion //#region src/utils/sequence.ts /** * Generates a sequence of numbers as an array. * @param {number} repeat - The end number of the sequence (inclusive). * @param {number} [begin=1] - The starting number of the sequence (inclusive). * @returns {number[]} An array of numbers representing the sequence. * @example * sequence(5, 1) // returns [1, 2, 3, 4, 5] * sequence(5, 3) // returns [3, 4, 5] * sequence(3) // returns [1, 2, 3] */ function sequence(repeat, begin = 1) { return Array(repeat).fill(0).map((_, i) => i + 1).filter((i) => i >= begin); } //#endregion //#region src/utils/svgs.ts /** * Converts a raw SVG string into a data URL that can be used in CSS or as an image source. * @param {string} str - The raw SVG string to convert. * @returns {string} A data URL representation of the SVG. */ function svgToUrl(str) { const cleaned = str.replace(/[\t\n\r]/gim, "").replace(/\s\s+/g, " "); return `data:image/svg+xml;charset=UTF-8,${encodeURIComponent(cleaned).replace(/\(/g, "%28").replace(/\)/g, "%29")}`; } /** * Determines the length of a ruler tick mark based on its position. * @internal * @param {number} len - The position (in pixels) of the tick mark. * @returns {number} The length of the tick mark. */ var rulerMarkerLength = (len) => { if (len % 100 === 0) return 15; if (len % 50 === 0) return 10; if (len % 25 === 0) return 5; return 2.5; }; /** * Generates an SVG of a rectangle with rulers along the horizontal and vertical axes. * @param {number} width - The width of the rectangle. * @param {number} height - The height of the rectangle. * @param {string} bgColor - The background color. * @param {string} textColor - The color for the text and ruler markings. * @returns A Lit template for the SVG with rulers. */ function rectangleRuler(width, height, bgColor, textColor) { let markers = ""; for (let x = 0; x <= width; x += 5) { const tick = html` `; markers += tick; if (x !== 0 && x % 50 === 0) { const label = html` ${x} `; markers += label; } } for (let y = 0; y <= height; y += 5) { const tick = html` `; markers += tick; if (y !== 0 && y % 50 === 0) { const label = html` ${y} `; markers += label; } } return html` ${width}x${height} ${markers} `; } /** * Creates a data URL for a placeholder image with specified dimensions and colors. * @param {number} width - The width of the placeholder. * @param {number} height - The height of the placeholder. * @param {string} [bgColor='#0F1C3F'] - The background color. * @param {string} [textColor='#ECEAD9'] - The text and ruler color. * @returns {string} A data URL for the generated placeholder SVG. */ function placeholder(width, height, bgColor = "#0F1C3F", textColor = "#ECEAD9") { return svgToUrl(rectangleRuler(width, height, bgColor, textColor)); } var backgrounds = _.values(colors).map((i) => i["900"]); var widths = [ 400, 600, 900, 1200, 1400, 1600, 1970 ]; var heights = [ 600, 800, 1e3, 1200, 1400, 2e3, 2600 ]; /** * Generates a placeholder image data URL with random dimensions and a random background color. * @returns {string} A data URL for the randomly generated placeholder SVG. */ function randomPlaceholder() { const randomWidth = Math.floor(Math.random() * widths.length); const randomHeight = Math.floor(Math.random() * heights.length); const randomColor = Math.floor(Math.random() * backgrounds.length); return placeholder(widths[randomWidth], heights[randomHeight], backgrounds[randomColor]); } //#endregion //#region src/ui/events/common.ts /** * An event handler that transforms vertical mouse wheel scrolling into horizontal scrolling. * This is used for Left-to-Right (LTR) fluid reading modes. * @param {WheelEvent} event - The wheel event. */ function transformScrollToHorizontal(event) { if (!event.deltaY) return; event.currentTarget.scrollLeft += event.deltaY + event.deltaX; event.preventDefault(); } /** * An event handler that transforms vertical mouse wheel scrolling into horizontal scrolling in the reverse direction. * This is used for Right-to-Left (RTL) fluid reading modes. * @param {WheelEvent} event - The wheel event. */ function transformScrollToHorizontalReverse(event) { if (!event.deltaY) return; event.currentTarget.scrollLeft -= event.deltaY - event.deltaX; event.preventDefault(); } //#endregion //#region src/ui/styles/navbar.css?inline var navbar_default = ":host {\r\n --nav-collapsed-size: 34px;\r\n --nav-expanded-size: 200px;\r\n --header-height: 80px;\r\n}\r\n#Navigation {\r\n color: var(--theme-text-color);\r\n background-color: var(--theme-hightlight-color);\r\n overflow: hidden;\r\n display: flex;\r\n box-sizing: border-box;\r\n gap: 5px;\r\n white-space: nowrap;\r\n text-align: center;\r\n line-height: 0;\r\n transition: all 0.3s ease;\r\n position: fixed;\r\n z-index: 1000;\r\n}\r\n#Thumbnails {\r\n flex-grow: 1;\r\n display: flex;\r\n gap: 5px;\r\n justify-content: flex-start;\r\n}\r\n#Navigation.horizontal #Thumbnails {\r\n flex-direction: row;\r\n overflow-x: auto;\r\n overflow-y: hidden;\r\n}\r\n#Navigation.vertical #Thumbnails {\r\n flex-direction: column;\r\n overflow-y: auto;\r\n overflow-x: hidden;\r\n justify-content: flex-start;\r\n}\r\n#Navigation.left #Thumbnails {\r\n direction: rtl;\r\n}\r\n:host(:not([forceExpanded])) #Navigation:not(:hover) #Thumbnails {\r\n display: none;\r\n}\r\n#NavigationCounters {\r\n flex-shrink: 0; /* Prevent this from shrinking */\r\n padding: 5px;\r\n line-height: 1rem;\r\n text-align: center;\r\n white-space: nowrap;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n gap: 0.5rem;\r\n}\r\n/* == Horizontal Orientation (for top/bottom position) == */\r\n#Navigation.horizontal {\r\n flex-direction: column;\r\n height: var(--nav-collapsed-size);\r\n width: 100%;\r\n left: 0;\r\n right: 0;\r\n}\r\n:host([forceExpanded]) #Navigation.horizontal,\r\n#Navigation.horizontal:hover {\r\n height: var(--nav-expanded-size);\r\n}\r\n#Navigation.bottom {\r\n bottom: 0;\r\n}\r\n/* == Vertical Orientation (for left/right position) == */\r\n#Navigation.vertical {\r\n flex-direction: row;\r\n width: var(--nav-collapsed-size);\r\n height: 100%;\r\n bottom: 0;\r\n transition:\r\n top 0.3s ease,\r\n height 0.3s ease,\r\n width 0.3s ease;\r\n}\r\n:host([forceExpanded]) #Navigation.vertical,\r\n#Navigation.vertical:hover {\r\n width: var(--nav-expanded-size);\r\n}\r\n#Navigation.left {\r\n left: 0;\r\n flex-direction: row-reverse;\r\n}\r\n#Navigation.right {\r\n right: 0;\r\n}\r\n#Navigation.vertical #NavigationCounters {\r\n writing-mode: vertical-rl;\r\n transform: rotate(180deg);\r\n}\r\n#Navigation.right #NavigationCounters {\r\n transform: rotate(0deg);\r\n}\r\n/* Adjust for header visibility */\r\n#Navigation.vertical.header {\r\n top: var(--header-height);\r\n height: calc(100% - var(--header-height));\r\n}\r\n\r\n#Navigation .Thumbnail {\r\n display: inline-flex;\r\n height: 150px;\r\n width: 150px;\r\n margin: 0 5px;\r\n position: relative;\r\n justify-content: center;\r\n align-items: center;\r\n}\r\n\r\n.ThumbnailIndex {\r\n color: var(--mov-color-on-loud);\r\n background-color: var(--mov-color-fill-loud);\r\n display: block;\r\n opacity: 0.9;\r\n position: absolute;\r\n left: 0;\r\n bottom: 30%;\r\n width: 100%;\r\n line-height: 1.2rem;\r\n text-align: center;\r\n font-weight: 600;\r\n z-index: 1;\r\n}\r\n.ThumbnailImg {\r\n cursor: pointer;\r\n display: inline-block;\r\n max-height: 150px;\r\n min-height: 150px;\r\n min-width: 80px;\r\n max-width: 150px;\r\n background-repeat: no-repeat;\r\n background-position: center;\r\n background-size: 48px 48px;\r\n}\r\n"; //#endregion //#region src/ui/Navbar.ts var Navbar = class Navbar extends i$1 { constructor(..._args) { super(..._args); this.mode = "bottom"; this.forceExpanded = false; this.isHiding = false; } static { this.styles = [r$4(navbar_default), i$3` #Navigation { transition: opacity 0.2s ease-in-out; } #Navigation.hiding { opacity: 0; /* Disable transition during position change to avoid animating the hide */ transition: none; } .Thumbnail .ThumbnailImg[src=''], .Thumbnail .ThumbnailImg:not([src]) { background-image: url('${r$4(svgToUrl(IconPhoto$1))}'); } .Thumbnail .ThumbnailImg.imgBroken { background-image: url('${r$4(svgToUrl(IconPhotoOff$1))}'); } `]; } /** * Lit-element lifecycle method. * Hides the navbar just before its position is about to change. */ willUpdate(changedProperties) { if (changedProperties.has("mode")) this.isHiding = true; } /** * Lit-element lifecycle method. * Fades the navbar back in after it has been re-rendered in the new position. */ updated(changedProperties) { if (changedProperties.has("mode") && this.isHiding) setTimeout(() => { this.isHiding = false; }, 50); } /** * Renders the navigation bar, including page counters and thumbnails. * @returns The rendered template. */ render() { if (this.mode === "disabled") return A; const manga = getAppStateValue("manga"); const navClasses = { horizontal: this.mode === "bottom", vertical: this.mode !== "bottom", left: this.mode === "left", right: this.mode === "right", bottom: this.mode === "bottom", hiding: this.isHiding }; const images = getAppStateValue("images") || {}; const loaded = _.keys(images).filter((key) => { const idx = parseInt(key, 10); return idx >= (manga?.begin ?? 1) && idx <= (manga?.pages ?? 1) && images[idx]?.status === "loaded"; }).length; return b$1` `; } }; __decorate([n$1({ type: String })], Navbar.prototype, "mode", void 0); __decorate([n$1({ type: Boolean })], Navbar.prototype, "forceExpanded", void 0); __decorate([r$1()], Navbar.prototype, "isHiding", void 0); Navbar = __decorate([t$1("navbar-thumbnails"), (0, import_lib.useStores)(settings, locale, appState)], Navbar); //#endregion //#region src/ui/SettingsPanelGeneral.ts function settingsScope() { const value = isSettingsLocal() ? "true" : "false"; return b$1`
${getLocaleString("SCOPE")}
`; } function localeSelector() { return locales.map((locale) => b$1` `); } function language() { return b$1`
${getLocaleString("LANGUAGE")}
`; } var SettingsPanelGeneral_default = () => b$1`${settingsScope()} ${language()}`; //#endregion //#region src/ui/SettingsPanelLoading.ts function loadMode() { return b$1`
${getLocaleString("DEFAULT_LOAD_MODE")}
`; } function loadSpeed() { return b$1`
${getLocaleString("LOAD_SPEED")}
`; } var SettingsPanelLoading_default = () => b$1`${loadMode()} ${loadSpeed()}`; //#endregion //#region src/ui/SettingsPanelOthers.ts function checkboxOptions() { return b$1`
${getLocaleString("FIT_WIDTH_OVERSIZED")}
${getLocaleString("DOWNLOAD_IMAGES")}
${getLocaleString("HIDE_CONTROLS")}
${getLocaleString("LAZY_LOAD_IMAGES_ENABLE")}
`; } function lazyLoad() { return b$1`
${getLocaleString("LAZY_LOAD_IMAGES")} ${getSettingsValue("lazyStart")}
`; } function headerType() { return b$1`
${getLocaleString("HEADER_TYPE")}
`; } function pagination() { return b$1` `; } function navbarType() { return b$1` `; } function autoScroll() { return b$1`
${getLocaleString("AUTO_SCROLL_HEIGHT")} ${getSettingsValue("scrollHeight")}px
`; } var SettingsPanelOthers_default = () => b$1`${checkboxOptions()} ${pagination()} ${lazyLoad()} ${headerType()} ${navbarType()} ${autoScroll()}`; //#endregion //#region src/ui/events/theming.ts /** * Toggles the color scheme between 'dark' and 'light' and applies the corresponding class to the root element. */ function changeColorScheme() { const isDark = getSettingsValue("colorScheme") === "dark"; saveSettingsValue("colorScheme", isDark ? "light" : "dark"); document.documentElement.classList.remove(isDark ? "dark" : "light"); document.documentElement.classList.add(getSettingsValue("colorScheme")); } /** * Event handler for changing the theme color via a text input or color picker. * @param {Event} event - The input or change event from a color input element. */ function changeTheme(event) { saveSettingsValue("theme", event instanceof CustomEvent ? event.detail.value : event.currentTarget.value); } //#endregion //#region src/ui/SettingsPanelTheme.ts function theme() { return b$1`
${_.values(sample).map((c) => b$1``)}
${getLocaleString("THEME_HUE")} & ${getLocaleString("THEME_SHADE")}
`; } //#endregion //#region src/ui/SettingsPanelZoom.ts /** * Renders the control for setting the default zoom mode (Percent, Fit Width, Fit Height). * @returns The Lit template for the default zoom mode setting. */ function defaultZoomMode() { return b$1`
${getLocaleString("DEFAULT_ZOOM_MODE")}
`; } /** * Renders the control for setting the default zoom value as a percentage. * This control is only visible when the zoom mode is set to 'percent'. * @returns The Lit template for the default zoom value setting. */ function zoomValue() { return b$1`
${getLocaleString("DEFAULT_ZOOM")} ${getSettingsValue("zoomValue")}%
`; } /** * Renders the control for setting the minimum zoom level for images. * @returns The Lit template for the minimum zoom setting. */ function minZoom() { return b$1`
${getLocaleString("MINIMUM_ZOOM")} ${getSettingsValue("minZoom")}%
`; } /** * Renders the control for setting the zoom step value, which determines the increment/decrement for zoom actions. * @returns The Lit template for the zoom step setting. */ function zoomStep() { return b$1`
${getLocaleString("ZOOM_STEP")} ${getSettingsValue("zoomStep")}%
`; } /** * Renders the control for setting the default view mode (Vertical, WebComic, Fluid LTR, Fluid RTL). * @returns The Lit template for the default view mode setting. */ function viewMode() { return b$1`
${getLocaleString("DEFAULT_VIEW_MODE")}
`; } /** * Renders the zoom and view settings panel by combining all the individual setting controls. * @returns The Lit template for the entire zoom settings panel. */ var SettingsPanelZoom_default = () => b$1`${defaultZoomMode()} ${zoomValue()} ${minZoom()} ${zoomStep()} ${viewMode()}`; //#endregion //#region src/ui/styles/settings.css?inline var settings_default = "#SettingsPanel {\r\n color: var(--theme-text-color);\r\n}\r\n\r\n#SettingsPanel fieldset {\r\n border: 1px solid var(--theme-body-text-color);\r\n padding: 3px;\r\n border-radius: 10px;\r\n}\r\n\r\n#SettingsPanel .ControlLabel {\r\n display: flex;\r\n flex-flow: row wrap;\r\n justify-content: space-between;\r\n align-items: center;\r\n padding: 2px;\r\n}\r\n\r\n#SettingsPanel .ControlLabelItem {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n}\r\n\r\n#SettingsPanel .ControlLabelItem:not(.show) {\r\n display: none;\r\n}\r\n\r\n#SettingsPanel input[type=\"range\"] {\r\n width: 100%;\r\n}\r\n\r\n#SettingsPanel .RangeValue {\r\n display: inline-block;\r\n color: var(--mov-color-on-loud);\r\n line-height: 20px;\r\n text-align: center;\r\n border-radius: 3px;\r\n background: var(--mov-color-fill-loud);\r\n padding: 2px 5px;\r\n margin-left: 8px;\r\n}\r\n\r\n#SettingsPanel datalist {\r\n display: flex;\r\n flex-direction: row;\r\n justify-content: space-between;\r\n width: 100%;\r\n}\r\n\r\n#SettingsPanel datalist option {\r\n padding: 0;\r\n writing-mode: vertical-lr;\r\n}\r\n\r\n#ThemeSelector {\r\n width: 110px;\r\n}\r\n\r\n#ColorRecommendations {\r\n display: flex;\r\n flex-direction: row;\r\n flex-wrap: wrap;\r\n gap: 2px;\r\n}\r\n#Chapter:not(.Vertical) ~ #SettingsPanel .verticalSeparator {\r\n display: none;\r\n}\r\n\r\n#ColorScheme {\r\n padding: 5px;\r\n min-height: 28px;\r\n min-width: 28px;\r\n}\r\n\r\n#ResetSettings,\r\n#ResetSettings::part(base) {\r\n width: 100%;\r\n}\r\n"; //#endregion //#region src/ui/SettingsPanel.ts var SettingsPanel = class SettingsPanel extends i$1 { static { this.styles = [i$3` #SettingsPanel.mobile #SettingsPanelZoom, #SettingsPanel.mobile .fitIfOversize, #SettingsPanel.mobile .showThumbnails, #SettingsPanel.mobile .lazyLoadImages, #SettingsPanel.mobile .downloadZip, #SettingsPanel.mobile .minZoom, #SettingsPanel.mobile .zoomStep, #SettingsPanel.mobile .headerType, #SettingsPanel.mobile .navbarType, #SettingsPanel.mobile .autoScroll { display: none; } `, r$4(settings_default)]; } render() { return b$1`

${getLocaleString("SETTINGS")}

${getLocaleString("BUTTON_RESET_SETTINGS")}
${getLocaleString("GENERAL")} ${SettingsPanelGeneral_default()}
${getLocaleString("THEME")} ${theme()}
${getLocaleString("LOADING")} ${SettingsPanelLoading_default()}
${getLocaleString("ZOOM")} ${SettingsPanelZoom_default()}
${getLocaleString("OTHERS")} ${SettingsPanelOthers_default()}
`; } }; SettingsPanel = __decorate([t$1("settings-panel"), (0, import_lib.useStores)(settings, locale, appState)], SettingsPanel); //#endregion //#region src/utils/request.ts /** * Fetches content from a URL and parses it as a DOM document. * @param {string} url - The URL to fetch. * @param {DOMParserSupportedType} format - The MIME type to use for parsing (e.g., 'text/html', 'text/xml'). * @returns {Promise} A promise that resolves with the parsed `Document` object. */ async function fetchText(url, format) { logScript("Fetching page: ", url); try { const html = await (await fetch(url)).text(); return new DOMParser().parseFromString(html, format); } catch (err) { logScript("Failed to fetch page: ", err); throw err; } } /** * Fetches a URL and parses it as an HTML document. * @param {string} url - The URL to fetch. * @returns {Promise} A promise that resolves with the parsed HTML `Document`. */ async function fetchHtml(url) { return fetchText(url, "text/html"); } /** * Fetches an HTML page and extracts the value of a specific attribute from a given element. * @param {string} url - The URL of the page to fetch. * @param {string} selector - The CSS selector for the target element. * @param {string} attribute - The name of the attribute to retrieve. * @returns {Promise} A promise that resolves with the attribute's value, or `null` if not found. */ async function getElementAttribute(url, selector, attribute) { try { return (await fetchHtml(url)).querySelector(selector)?.getAttribute(attribute); } catch (err) { logScript("Failed to get element attribute: ", err); return null; } } //#endregion //#region src/core/WorkerPool.ts var WorkerPool = class { constructor(speed, customDelay) { this.queue = []; this.activeCount = 0; this.lastRunTime = 0; const config = { Safe: { concurrency: 5, delay: 1e3 }, Standard: { concurrency: 5, delay: 500 }, Faster: { concurrency: 10, delay: 500 }, Extreme: { concurrency: 10, delay: 250 }, All: { concurrency: 20, delay: 50 } }[speed]; this.maxConcurrency = config.concurrency; this.minDelay = customDelay ?? config.delay; } add(task) { this.queue.push(task); this.runNext(); } async runNext() { if (this.activeCount >= this.maxConcurrency || this.queue.length === 0) return; const timeSinceLastRun = Date.now() - this.lastRunTime; if (timeSinceLastRun < this.minDelay) { setTimeout(() => this.runNext(), this.minDelay - timeSinceLastRun); return; } const task = this.queue.shift(); if (task) { this.activeCount += 1; this.lastRunTime = Date.now(); try { await task(); } finally { this.activeCount -= 1; this.runNext(); } } } }; //#endregion //#region src/core/Image.ts var pool; /** * Normalizes a URL by trimming whitespace and ensuring it starts with a protocol. * @param {string} url - The URL to normalize. * @returns {string} The normalized URL. */ function normalizeUrl(url) { if (url) { let uri = url.trim(); if (uri.startsWith("//")) uri = `https:${uri}`; return uri; } return ""; } /** * Fetches an image, converts it to a data URL, and adds it to the application state. * @param {IMangaImages | IMangaPages} manga - The manga object with image loading configurations. * @param {number} index - The page number of the image. * @param {string} imageSrc - The source URL of the image. */ async function addImg(manga, index, imageSrc) { const image = getAppStateValue("images")?.[index]; if (image?.status && image.status !== "pending") return; changeImage(index, () => ({ status: "loading" })); pool.add(async () => { let src = normalizeUrl(imageSrc); let blob; try { const response = await fetch(src, manga.fetchOptions); if (response.ok) { const contentType = response.headers.get("content-type"); if (contentType?.startsWith("image/")) { blob = await response.blob(); src = await blobUtil.blobToDataURL(blob); } else logScript("Fetched content is not an image", contentType); } else logScript("Fetch failed with status", response.status); } catch (e) { logScript("Failed to fetch image", e); } changeImage(index, () => ({ src, blob, status: "loaded" })); logScriptVerbose("Loaded Image:", index, "Source:", src); }); if (manga.pages === index) removeURLBookmark(); } /** * Fetches a page URL to find the image source within it, then adds the image to the application state. * @param {IMangaPages} manga - The manga object with page loading configurations. * @param {number} index - The page number. * @param {string} pageUrl - The URL of the page containing the image. */ async function addPage(manga, index, pageUrl) { const image = getAppStateValue("images")?.[index]; if (image?.status && image.status !== "pending") return; changeImage(index, () => ({ status: "loading" })); pool.add(async () => { try { const imageSrc = await getElementAttribute(pageUrl, manga.img, manga.lazyAttr ?? "src"); if (imageSrc) { changeImage(index, () => ({ status: "pending" })); await addImg(manga, index, imageSrc); } else changeImage(index, () => ({ status: "error" })); } catch (e) { logScript("Failed to get page attribute", e); changeImage(index, () => ({ status: "error" })); } }); } /** * Loads a manga by fetching individual page URLs and extracting the image source from each. * @param {number} begin - The starting page number. * @param {IMangaPages} manga - The manga object with a list of page URLs. */ function loadMangaPages(begin, manga) { sequence(manga.pages, begin).filter((_index, position) => !(manga.lazy ?? getSettingsValue("lazyLoadImages")) || position <= getSettingsValue("lazyStart")).forEach((index) => { addPage(manga, index, manga.listPages[index - 1]); }); } /** * Loads a manga from a direct list of image URLs. * @param {number} begin - The starting page number. * @param {IMangaImages} manga - The manga object with a list of image URLs. */ function loadMangaImages(begin, manga) { sequence(manga.pages, begin).filter((_index, position) => !(manga.lazy ?? getSettingsValue("lazyLoadImages")) || position <= getSettingsValue("lazyStart")).forEach((index) => { addImg(manga, index, manga.listImages[index - 1]); }); } /** * The main entry point for loading manga images based on the detected loading strategy. * It determines whether to load from a list of images, a list of pages, or use a brute-force method. * It also sets up a listener to lazy-load subsequent images as the user scrolls. * @returns {Promise} */ async function loadImages() { await waitForFunc(() => getAppStateValue("manga") !== void 0); const manga = getAppStateValue("manga"); const begin = manga.begin ?? 1; pool = new WorkerPool(getSettingsValue("loadSpeed"), manga.timer); logScriptVerbose("Loading Images"); logScriptVerbose(`Speed: ${getSettingsValue("loadSpeed")}`); logScriptVerbose(`Lazy: ${manga.lazy ?? getSettingsValue("lazyLoadImages")}, Starting from: ${getSettingsValue("lazyStart")}`); applyZoom(); if (isImagesManga(manga)) { logScriptVerbose("Method: Images:", manga.listImages); loadMangaImages(begin, manga); } else if (isPagesManga(manga)) { logScriptVerbose("Method: Pages:", manga.listPages); loadMangaPages(begin, manga); } else if (isBruteforceManga(manga)) { logScriptVerbose("Method: Brute Force"); manga.bruteForce({ begin, addImg, loadImages(list) { loadMangaImages(begin, { ...manga, listImages: list }); }, loadPages(list, img, lazyAttr) { loadMangaPages(begin, { ...manga, listPages: list, img, lazyAttr }); }, wait: 0 }); } else logScript("No Loading Method Found"); appState.listen((value, oldValue, changedKey) => { if (changedKey === "currentPage" && value.currentPage > oldValue.currentPage) for (let i = value.currentPage; i < Math.min(value.currentPage + 5, manga.pages + 1); i++) { if (value.images?.[i]?.src !== void 0 || value.images?.[i]?.status === "loading") continue; if (isImagesManga(manga)) addImg(manga, i, manga.listImages[i - 1]); else if (isPagesManga(manga)) addPage(manga, i, manga.listPages[i - 1]); } }); } //#endregion //#region src/ui/events/currentpage.ts /** * Computes which page is currently considered "active" in the viewport. * The logic identifies which page's leading edge (top for vertical, left/right for horizontal) * has most recently passed the center of the viewport. * @internal * @returns {number | null} The current page number, or `null` if it cannot be determined. */ function computeCurrentPage() { const pages = getAppStateValue("images"); if (!pages) return null; const viewMode = getSettingsValue("viewMode"); const isHorizontal = viewMode === "FluidLTR" || viewMode === "FluidRTL"; const isRTL = viewMode === "FluidRTL"; const viewportCenterY = window.innerHeight / 2; const viewportCenterX = window.innerWidth / 2; let best = null; for (const index in pages) { const image = pages[index].ref?.value; if (!image) continue; const rect = image?.getBoundingClientRect(); let edge; if (isHorizontal) if (isRTL) edge = rect.right; else edge = rect.left; else edge = rect.top; if (isHorizontal ? edge <= viewportCenterX : edge <= viewportCenterY) { if (!best || edge > best.edge) best = { index: parseInt(index, 10), edge }; } } if (!best) return getAppStateValue("manga")?.begin ?? 1; return best.index; } /** * Calculates the current page and updates the global application state if it has changed. * @internal */ function updateCurrentPage() { const page = computeCurrentPage(); if (page == null) return; if (getAppStateValue("currentPage") !== page) setAppStateValue("currentPage", page); } /** * Attaches the necessary scroll and resize listeners to trigger the current page calculation. * @internal */ function attachListeners() { const handler = _.throttle(() => { requestAnimationFrame(updateCurrentPage); }, 100); window.addEventListener("scroll", handler, { passive: true }); window.addEventListener("resize", handler); getAppStateValue("chapter").value?.addEventListener("scroll", handler, { passive: true }); requestAnimationFrame(updateCurrentPage); } /** * Initializes the current page tracking functionality. * It waits until the main application has rendered and then attaches the necessary event listeners. */ function trackCurrentPage() { if (!getAppStateValue("chapter").value) { setTimeout(trackCurrentPage, 50); return; } attachListeners(); } //#endregion //#region src/ui/events.ts /** * Event handler for window resize and orientation change events. * It recalculates the zoom to ensure the content fits the new window dimensions. */ var updateOnResize = _.debounce(() => { setAppStateValue("device", getDevice()); applyZoom(); }, 200); /** * Initializes all global event listeners for the application. * This function sets up headroom for the header, keyboard shortcuts, * auto-scrolling, current page tracking, and responsive device detection on resize. */ async function events() { await waitForFunc(() => getAppStateValue("manga") !== void 0); keybindings(); window.addEventListener("resize", updateOnResize); window.addEventListener("orientationchange", updateOnResize); autoscroll(); trackCurrentPage(); } //#endregion //#region src/utils/urls.ts /** * Regular expression to match an object URL (blob URL). * @type {RegExp} */ var objectURLRegex = /^blob:(.+?)\/(.+)$/; /** * Checks if a given URL is a Base64 image data URL. * @param {string} imageUrl - The URL to check. * @returns {boolean} `true` if the URL is a Base64 image, `false` otherwise. */ function isBase64ImageUrl(imageUrl) { return /^data:image\/(png|jpg|jpeg|gif|svg)/.test(imageUrl); } /** * Checks if a given URL is an object URL (blob URL). * @param {string} url - The URL to check. * @returns {boolean} `true` if the URL is an object URL, `false` otherwise. */ function isObjectURL(url) { return objectURLRegex.test(url); } //#endregion //#region src/ui/events/individual.ts /** * Appends a cache-busting query parameter to a URL. * @internal * @param {string} src - The original image source URL. * @param {number} repeat - The current reload attempt number. * @returns {string} The new URL with the `forceReload` parameter. */ function invalidateImageCache(src, repeat) { const url = src.replace(/[?&]forceReload=\d+$/, ""); return `${url + (!url.includes("?") ? "?" : "&")}forceReload=${repeat}`; } /** * Attempts to reload a broken image with a cache-busting parameter. * It will only attempt to reload up to 5 times. * @internal * @param {number} index - The page index of the image to reload. * @param {HTMLImageElement} img - The `` element to reload. */ function reloadImage(index, img) { const image = getAppStateValue("images")?.[index]; if (!image?.src) return; const repeat = (image.reload ?? 0) + 1; if (repeat > getSettingsValue("maxReload")) { logScript(`Stopped reloading Page ${index} after ${repeat} attempts`); return; } logScript(`Reloading Page ${index} (Attempt ${repeat})`, img); img?.removeAttribute("src"); if (isBase64ImageUrl(image.src) || isObjectURL(image.src)) { changeImage(index, () => ({ reload: repeat })); img?.setAttribute("src", image.src); } else { const src = invalidateImageCache(image.src, repeat); changeImage(index, () => ({ reload: repeat, src })); img?.setAttribute("src", src); } } /** * Event handler for the reload button on an individual page. * @param {Event} event - The click event from the reload button. */ function buttonReloadPage(event) { const button = event.currentTarget; const index = parseInt(button.value, 10); const img = getAppStateValue("images")?.[index]?.ref?.value; if (img) reloadImage(index, img); } /** * Event handler for the hide button on an individual page, which toggles the visibility of the page container. * @param {Event} event - The click event from the hide button. */ function buttonHidePage(event) { const button = event.currentTarget; changeImage(parseInt(button.value, 10), (image) => ({ hide: !image.hide })); } /** * Event handler for when an image successfully loads. * It updates the application state with the image's dimensions, attempts to create a blob for downloading, * and updates the overall loading progress bar. * @param {Event} event - The load event from an `` element. */ function imageLoaded(event) { const img = event.currentTarget; changeImage(parseInt(img.id.replace("PageImg", ""), 10), (_image) => ({ ...calculatePageZoom({ naturalWidth: img.naturalWidth, naturalHeight: img.naturalHeight }), status: "loaded", doublePage: img.naturalWidth > img.naturalHeight })); const manga = getAppStateValue("manga"); const images = getAppStateValue("images") || {}; const loaded = _.keys(images).filter((key) => { const idx = parseInt(key, 10); return idx >= (manga?.begin ?? 1) && idx <= (manga?.pages ?? 1) && images[idx]?.status === "loaded"; }).length; const total = (manga?.pages ?? 1) - ((manga?.begin ?? 1) - 1); const percentage = Math.floor(loaded / total * 100); document.title = `(${percentage}%) ${getAppStateValue("manga")?.title}`; NProgress.configure({ showSpinner: false }).set(loaded / total); logScript(`Progress: ${percentage}%`); if (loaded === total) { logScript("Images Loading Complete"); setAppStateValue("download", "available"); if (getSettingsValue("downloadZip")) buttonStartDownload(); } } /** * Event handler for when an image fails to load. * It marks the image as broken and attempts to reload it. * @param {Event} event - The error event from an `` element. */ function imageLoadError(event) { const img = event.currentTarget; if (isEmpty(img.getAttribute("src"))) return; const index = parseInt(img.id.replace("PageImg", ""), 10); changeImage(index, () => ({ status: "error" })); reloadImage(index, img); } //#endregion //#region src/ui/events/size.ts /** * Event handler to zoom in on an individual page by a percentage step. * @param {Event} event - The click event from the zoom-in button. */ function buttonZoomIn(event) { const button = event.currentTarget; const index = parseInt(button.value, 10); const images = getAppStateValue("images"); const img = getAppStateValue("images")?.[index]; if (img?.naturalWidth) setAppStateValue("images", { ...images, [index]: { ...img, width: (img?.width || img?.naturalWidth) * (1 + getSettingsValue("zoomStep") / 100), height: void 0 } }); } /** * Event handler to zoom out on an individual page by a percentage step. * @param {Event} event - The click event from the zoom-out button. */ function buttonZoomOut(event) { const button = event.currentTarget; const index = parseInt(button.value, 10); const images = getAppStateValue("images"); const img = getAppStateValue("images")?.[index]; if (img?.naturalWidth) setAppStateValue("images", { ...images, [index]: { ...img, width: (img?.width || img?.naturalWidth) * (1 - getSettingsValue("zoomStep") / 100), height: void 0 } }); } /** * Event handler to restore the zoom of an individual page to 100%. * @param {Event} event - The click event from the restore zoom button. */ function buttonRestoreZoom(event) { const button = event.currentTarget; const index = parseInt(button.value, 10); const images = getAppStateValue("images"); const img = getAppStateValue("images")?.[index]; if (img) setAppStateValue("images", { ...images, [index]: { ...img, width: void 0, height: void 0 } }); } /** * Event handler to fit an individual page to the screen width. * @param {Event} event - The click event from the fit-to-width button. */ function buttonZoomWidth(event) { const button = event.currentTarget; const index = parseInt(button.value, 10); const images = getAppStateValue("images"); const img = getAppStateValue("images")?.[index]; if (img) setAppStateValue("images", { ...images, [index]: { ...img, width: window.innerWidth + (getSettingsValue("navbar") === "left" || getSettingsValue("navbar") === "right" ? -34 : 0), height: void 0 } }); } /** * Event handler to fit an individual page to the screen height. * @param {Event} event - The click event from the fit-to-height button. */ function buttonZoomHeight(event) { const button = event.currentTarget; const index = parseInt(button.value, 10); const images = getAppStateValue("images"); const img = getAppStateValue("images")?.[index]; if (img) setAppStateValue("images", { ...images, [index]: { ...img, width: void 0, height: window.innerHeight + (getSettingsValue("navbar") === "bottom" ? -34 : 0) } }); } //#endregion //#region src/ui/MangaPage.ts /** * Calculates the inline styles for a manga page's image based on the current zoom settings. * This function determines the width and height of the image according to the global * zoom mode ('width', 'height', 'percent') and zoom value. * * @param {number} index - The page number for which to calculate the style. * @returns {StyleInfo} A Lit `StyleInfo` object to be used with the `styleMap` directive. */ function getImageStyle(index) { const image = getAppStateValue("images")?.[index]; const isBook = getSettingsValue("viewMode").match(/^(Book|Manga)$/); const isGallery = getSettingsValue("viewMode") === "Gallery"; const isFluid = getSettingsValue("viewMode").startsWith("Fluid"); const withNavbar = getSettingsValue("navbar") === "bottom"; return { width: image?.width ? `${image.width}px` : "auto", height: image?.height ? `${image.height}px` : "auto", "max-height": isFluid ? `${window.innerHeight + (withNavbar ? -34 : 0)}px` : void 0, "min-width": !isBook && !isGallery ? `${getSettingsValue("minZoom")}vw` : void 0 }; } /** * Generates the complete list of manga pages as a Lit `TemplateResult`. * Each page includes a set of controls (bookmark, zoom, hide, reload) and the page image itself. * A separator is also rendered between pages. * * @param {number} times - The total number of pages in the manga. * @param {number} begin - The starting page number. * @returns An array of Lit `TemplateResult` objects, one for each page. */ var listPages = (times, begin) => { return sequence(times, begin).map((index) => { if (!getAppStateValue("images")?.[index]?.ref) changeImage(index, (_image) => ({ ref: e$6() })); let singlePageCount = 0; for (let i = index - 1; i >= begin; i--) { if (getAppStateValue("images")?.[i].doublePage) break; if (!getAppStateValue("images")?.[i].doublePage) singlePageCount++; } const isDblPage = getAppStateValue("images")?.[index].doublePage ?? false; const isBookMode = getSettingsValue("viewMode") === "Book"; const isLeftPage = !isDblPage && (isBookMode ? singlePageCount % 2 === 0 : singlePageCount % 2 === 1); const isRightPage = !isDblPage && (isBookMode ? singlePageCount % 2 === 1 : singlePageCount % 2 === 0); return b$1`
${!getSettingsValue("viewMode").match(/^(Book|Manga)$/) ? b$1` ` : b$1` `} ${index}
Page ${index}
[ ${index === times ? getLocaleString("END") : `${index} / ${times}`} ]
`; }); }; //#endregion //#region src/ui/Reader.ts /** * Renders the main reader area where manga pages are displayed. * This component is responsible for laying out the pages according to the selected view mode * and applying appropriate scrolling behavior, such as transforming vertical scroll to horizontal for fluid modes. * * @param {IManga} manga - The manga object containing the page count and starting page number. * @returns A Lit `TemplateResult` representing the reader's main container. */ var Reader = (manga) => b$1`
{ if (getSettingsValue("viewMode") === "FluidLTR") transformScrollToHorizontal(e); else if (getSettingsValue("viewMode") === "FluidRTL") transformScrollToHorizontalReverse(e); }} > ${listPages(manga.pages, manga.begin ?? 0)}
`; //#endregion //#region src/ui/styles/index.ts /** * @file This module serves as the central aggregator for all the application's CSS styles. * It imports individual CSS files as inline strings, defines base theme variables for light and dark modes, * and combines them into a single CSS template literal that can be injected into the application's shadow DOM. */ var styles_default = i$3` .PageContent .PageImg[src=''], .PageContent .PageImg:not([src]) { background-image: url('${r$4(svgToUrl(IconPhoto$1))}'); } .PageContent .PageImg.imgBroken { background-image: url('${r$4(svgToUrl(IconPhotoOff$1))}'); } ${r$4(":root {\r\n /* Theme Colors */\r\n --theme-primary-color: #007bff; /* Example primary color */\r\n --theme-primary-text-color: #ffffff; /* Example primary text color */\r\n --theme-secondary-color: #6c757d; /* Example secondary color */\r\n --theme-secondary-text-color: #ffffff; /* Example secondary text color */\r\n\r\n /* Dark Theme */\r\n --theme-body-background: #212529;\r\n --theme-body-text-color: #f8f9fa;\r\n --theme-text-color: #f8f9fa;\r\n --theme-background-color: #212529;\r\n --theme-hightlight-color: #343a40;\r\n --theme-border-color: #495057;\r\n\r\n /* Custom Color Scales (based on gradient) */\r\n --mov-color-fill-quiet: #ced4da;\r\n --mov-color-fill-normal: #adb5bd;\r\n --mov-color-fill-loud: #007bff;\r\n --mov-color-border-quiet: #adb5bd;\r\n --mov-color-border-normal: #6c757d;\r\n --mov-color-border-loud: #495057;\r\n --mov-color-on-quiet: #343a40;\r\n --mov-color-on-normal: #ffffff;\r\n --mov-color-on-loud: #ffffff;\r\n\r\n --mov-color-mix-hover: rgba(0, 0, 0, 0.08);\r\n --mov-color-mix-active: rgba(0, 0, 0, 0.16);\r\n}\r\n\r\n.light {\r\n /* Light Theme */\r\n --theme-body-background: #f8f9fa;\r\n --theme-body-text-color: #212529;\r\n --theme-text-color: #212529;\r\n --theme-background-color: #f8f9fa;\r\n --theme-hightlight-color: #e9ecef;\r\n --theme-border-color: #dee2e6;\r\n\r\n /* Custom Color Scales (based on gradient) */\r\n --mov-color-fill-quiet: #f8f9fa;\r\n --mov-color-fill-normal: #e9ecef;\r\n --mov-color-fill-loud: #007bff;\r\n --mov-color-border-quiet: #e9ecef;\r\n --mov-color-border-normal: #dee2e6;\r\n --mov-color-border-loud: #adb5bd;\r\n --mov-color-on-quiet: #6c757d;\r\n --mov-color-on-normal: #ffffff;\r\n --mov-color-on-loud: #ffffff;\r\n\r\n --mov-color-mix-hover: rgba(0, 0, 0, 0.1);\r\n --mov-color-mix-active: rgba(0, 0, 0, 0.2);\r\n}\r\n")} ${r$4("/* Simple Normalizer */\r\n#MangaOnlineViewer {\r\n margin: 0;\r\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\r\n font-size: 16px;\r\n line-height: 20px;\r\n color: var(--theme-body-text-color);\r\n background-color: var(--theme-body-background);\r\n padding: 0;\r\n}\r\n\r\na,\r\na:link,\r\na:visited,\r\na:active,\r\na:focus {\r\n color: var(--theme-body-text-color);\r\n text-decoration: none;\r\n}\r\n\r\nimg {\r\n height: auto;\r\n vertical-align: middle;\r\n border: 0 none;\r\n}\r\n")} ${r$4(":root:not(.light, .dark) {\r\n --theme-body-background: #25262b;\r\n --theme-body-text-color: #c1c2c5;\r\n --theme-text-color: #c1c2c5;\r\n --theme-primary-color: #1a1b1e;\r\n --theme-primary-text-color: #c1c2c5;\r\n --theme-background-color: #25262b;\r\n --theme-hightlight-color: #2c2e33;\r\n --theme-border-color: #373a40;\r\n --theme-secondary-color: #2c2e33;\r\n --theme-secondary-text-color: #c1c2c5;\r\n}\r\n\r\n:host {\r\n all: initial;\r\n display: block;\r\n box-sizing: border-box;\r\n}\r\n\r\n#MangaOnlineViewer {\r\n text-decoration: none;\r\n color: var(--theme-body-text-color);\r\n background-color: var(--theme-body-background);\r\n box-sizing: border-box;\r\n min-height: 100vh;\r\n --mov-font-size-m: 16px;\r\n}\r\n\r\n#Chapter {\r\n display: grid;\r\n grid-template-columns: repeat(1, 1fr);\r\n min-width: 225px;\r\n box-sizing: border-box;\r\n}\r\n\r\n#Chapter.Vertical:has(+ #Navigation:not(.disabled)),\r\n#Chapter.WebComic:has(+ #Navigation:not(.disabled)) {\r\n padding-bottom: 31px;\r\n}\r\n\r\n#Chapter.Vertical .PageContent {\r\n margin-bottom: 8px;\r\n margin-top: 8px;\r\n}\r\n\r\n.closeButton {\r\n width: fit-content;\r\n height: fit-content;\r\n position: absolute;\r\n right: 10px;\r\n top: 10px;\r\n}\r\n\r\n.overlay {\r\n position: fixed;\r\n display: none;\r\n width: 100%;\r\n height: 100%;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n bottom: 0;\r\n background-color: rgba(0, 0, 0, 0.5);\r\n z-index: 950;\r\n cursor: pointer;\r\n}\r\n\r\n.overlay.visible {\r\n display: block;\r\n}\r\n\r\nselect {\r\n height: 20px;\r\n margin: 2px;\r\n}\r\n\r\n:not(.FluidRTL, .FluidLTR).fitWidthIfOversize .PageContent .PageImg {\r\n max-width: 100%;\r\n object-fit: contain;\r\n}\r\n\r\n.hideControls .PageFunctions {\r\n visibility: hidden;\r\n}\r\n")} ${r$4(".PageButton .icon-tabler {\r\n height: 1rem;\r\n width: 1rem;\r\n vertical-align: sub;\r\n}\r\n\r\n.PageButton,\r\n.PageButton:visited,\r\n.PageButton:link {\r\n cursor: pointer;\r\n border-radius: 5px;\r\n border-width: 1px;\r\n border-style: solid;\r\n padding: 2px;\r\n min-height: 32px;\r\n color: var(--mov-color-on-loud);\r\n background-color: var(--mov-color-fill-loud);\r\n border-color: var(--theme-border-color);\r\n text-decoration: none;\r\n}\r\n\r\n.PageButton:active,\r\n.PageButton:hover {\r\n opacity: 0.8;\r\n}\r\n\r\n.PageButton[selected] {\r\n background-color: var(--mov-color-fill-normal);\r\n color: var(--mov-color-on-normal);\r\n border: 1px solid var(--theme-border-color);\r\n}\r\n\r\n.PageButton.hidden {\r\n display: none;\r\n}\r\n\r\n.MangaPage {\r\n width: 100%;\r\n display: inline-block;\r\n text-align: center;\r\n line-height: 0;\r\n min-height: 22px;\r\n min-width: 100%;\r\n}\r\n\r\n.PageContent {\r\n text-align: center;\r\n display: inline-block;\r\n overflow-x: auto;\r\n max-width: 100%;\r\n transition: all 0.3s ease-in-out;\r\n height: 100%;\r\n overflow-y: hidden;\r\n}\r\n\r\n.MangaPage.hide .PageContent {\r\n height: 0;\r\n}\r\n\r\n.PageContent .PageImg[src=\"\"],\r\n.PageContent .PageImg:not([src]),\r\n.PageContent .PageImg.imgBroken {\r\n width: 40vw;\r\n height: 80vh;\r\n display: inline-block;\r\n background-position: center;\r\n background-repeat: no-repeat;\r\n background-size: 20%;\r\n background-color: var(--theme-hightlight-color);\r\n position: relative;\r\n text-align: center;\r\n line-height: 80vh;\r\n vertical-align: top;\r\n color: var(--theme-text-color);\r\n font-size: 16px;\r\n min-width: 40vw;\r\n min-height: 50vh;\r\n max-width: 100%;\r\n max-height: 100%;\r\n margin: 0;\r\n}\r\n\r\n.PageContent .PageImg[src=\"\"]:before,\r\n.PageContent .PageImg:not([src]):before,\r\n.PageContent .PageImg.imgBroken:before {\r\n content: attr(alt);\r\n position: absolute;\r\n top: 40%;\r\n left: 50%;\r\n transform: translate(-50%, -50%);\r\n white-space: pre-wrap;\r\n text-align: center;\r\n color: var(--theme-text-color);\r\n font-size: 16px;\r\n}\r\n\r\n.PageFunctions {\r\n font-family: monospace;\r\n display: flex;\r\n justify-content: flex-end;\r\n align-items: center;\r\n margin: 0;\r\n padding: 0;\r\n gap: 3px;\r\n position: absolute;\r\n right: 0;\r\n}\r\n\r\n.PageFunctions > .PageIndex {\r\n background-color: var(--mov-color-fill-loud);\r\n color: var(--mov-color-on-loud);\r\n min-width: 20px;\r\n text-align: center;\r\n display: inline-block;\r\n padding: 3px 5px;\r\n line-height: 1rem;\r\n border-radius: 5px;\r\n}\r\n\r\n.PageFunctions .PageButton {\r\n padding: 3px;\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n margin: 0;\r\n border-width: 0;\r\n min-height: auto;\r\n opacity: 0.5;\r\n}\r\n\r\n.PageFunctions:hover .PageButton {\r\n opacity: 1;\r\n}\r\n\r\n.PageFunctions .PageButton:hover {\r\n opacity: 0.9;\r\n}\r\n\r\n#Chapter.Vertical .separator {\r\n display: flex;\r\n align-items: center;\r\n text-align: center;\r\n font-style: italic;\r\n}\r\n\r\n#Chapter.Vertical .separator::before,\r\n#Chapter.Vertical .separator::after {\r\n content: \"\";\r\n flex: 1;\r\n border-bottom: 1px solid var(--theme-text-color);\r\n}\r\n\r\n#Chapter.Vertical.separator:not(:empty)::before {\r\n margin-right: 0.25em;\r\n}\r\n\r\n#Chapter.Vertical.separator:not(:empty)::after {\r\n margin-left: 0.25em;\r\n}\r\n\r\n#Chapter:not(.separator) .separator,\r\n#Chapter:not(.Vertical) .separator {\r\n display: none;\r\n}\r\n")} ${r$4("#Chapter.FluidLTR,\r\n#Chapter.FluidRTL {\r\n display: flex;\r\n overflow-x: auto;\r\n min-width: auto;\r\n\r\n .ZoomWidth {\r\n display: none;\r\n }\r\n\r\n .PageImg {\r\n min-width: unset;\r\n }\r\n\r\n .MangaPage {\r\n width: initial;\r\n min-width: fit-content;\r\n position: relative;\r\n }\r\n\r\n .MangaPage.DoublePage {\r\n grid-column: span 2;\r\n }\r\n}\r\n\r\n#Chapter.FluidLTR {\r\n flex-direction: row;\r\n\r\n .MangaPage .PageFunctions {\r\n right: auto;\r\n left: 0;\r\n direction: rtl;\r\n }\r\n}\r\n\r\n#Chapter.FluidRTL {\r\n flex-direction: row-reverse;\r\n}\r\n")} ${r$4("/* Book mode - Left to Right (Western comic/manga style) */\r\n#Chapter:where(.Book, .Manga) {\r\n display: grid;\r\n grid-template-columns: 1fr 1fr;\r\n grid-auto-flow: row;\r\n width: 100%;\r\n min-width: auto;\r\n gap: 0;\r\n}\r\n\r\n#Chapter:where(.Book, .Manga) .MangaPage {\r\n width: 100%;\r\n display: block;\r\n position: relative;\r\n min-height: 22px;\r\n overflow: hidden;\r\n}\r\n\r\n/* Default positioning for all controls in Book mode - top right */\r\n#Chapter:where(.Book, .Manga) .MangaPage .PageFunctions {\r\n top: 0;\r\n right: 0;\r\n left: auto;\r\n flex-direction: row;\r\n border-radius: 0 0 0 4px;\r\n}\r\n\r\n/* Left-side images - controls at top left with reversed order */\r\n#Chapter:where(.Book, .Manga) .MangaPage.LeftPage .PageFunctions {\r\n right: auto;\r\n left: 0;\r\n flex-direction: row-reverse;\r\n border-radius: 0 0 4px 0;\r\n}\r\n\r\n#Chapter:where(.Book, .Manga) .MangaPage.DoublePage {\r\n grid-column: span 2;\r\n}\r\n\r\n#Chapter:where(.Book, .Manga) .MangaPage .PageContent {\r\n display: flex;\r\n justify-content: center;\r\n align-items: center;\r\n flex-shrink: 0;\r\n overflow: hidden;\r\n}\r\n\r\n/* Left page - align image to the right (toward middle) */\r\n#Chapter:where(.Book, .Manga) .MangaPage.LeftPage .PageContent {\r\n justify-content: flex-end;\r\n padding-right: 0;\r\n}\r\n\r\n/* Right page - align image to the left (toward middle) */\r\n#Chapter:where(.Book, .Manga) .MangaPage.RightPage .PageContent {\r\n justify-content: flex-start;\r\n padding-left: 0;\r\n}\r\n\r\n/* Double page - center the image */\r\n#Chapter:where(.Book, .Manga) .MangaPage.DoublePage .PageContent {\r\n justify-content: center;\r\n}\r\n\r\n/* Manga mode - Right to Left (Traditional manga/comic style) */\r\n#Chapter.Manga {\r\n direction: rtl;\r\n}\r\n\r\n#Chapter.Manga .MangaPage {\r\n direction: ltr; /* Reset text direction for page contents */\r\n}\r\n")} ${r$4("#Chapter.Gallery {\r\n display: flex;\r\n flex-wrap: wrap;\r\n justify-content: center;\r\n gap: 10px;\r\n padding: 10px;\r\n}\r\n\r\n.Gallery .MangaPage {\r\n width: auto;\r\n min-width: unset;\r\n flex: 0 1 auto;\r\n}\r\n\r\n.Gallery .MangaPage .PageContent .PageImg {\r\n min-width: unset;\r\n}\r\n\r\n.Gallery .PageFunctions,\r\n.Gallery .separator {\r\n display: none;\r\n}\r\n")} ${r$4(media_default)} ${r$4("@-webkit-keyframes spin {\r\n to {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n\r\n@keyframes spin {\r\n to {\r\n transform: rotate(360deg);\r\n }\r\n}\r\n\r\n@-webkit-keyframes spin-reverse {\r\n 0% {\r\n transform: rotate(360deg);\r\n }\r\n\r\n to {\r\n transform: rotate(0);\r\n }\r\n}\r\n\r\n@keyframes spin-reverse {\r\n 0% {\r\n transform: rotate(360deg);\r\n }\r\n\r\n to {\r\n transform: rotate(0);\r\n }\r\n}\r\n\r\n.icon-tabler-loader-2,\r\n.animate-spin {\r\n -webkit-animation: spin 1s linear infinite;\r\n animation: spin 1s linear infinite;\r\n}\r\n\r\n.animate-spin-reverse {\r\n -webkit-animation: spin-reverse 1s linear infinite;\r\n animation: spin-reverse 1s linear infinite;\r\n}\r\n")} `; //#endregion //#region src/ui/themes.ts /** * Generates a CSS theme configuration for a specified selector and color theme. * This includes light and dark theme configurations, as well as various color scales and gradients. * * @param {string} [selector='#MangaOnlineViewer'] - The CSS selector to apply the theme styles to. Defaults to `#MangaOnlineViewer`. * @param {string} [hex=getSettingsValue('theme')] - The base hexadecimal color used to derive the theme colors. Defaults to the user's theme settings value. * @returns {string} A string of CSS styles, or an empty string if the gradient generation fails. */ var themesCSS = (selector = "#MangaOnlineViewer", hex = getSettingsValue("theme")) => { const gradient = generateColorGradient(hex); const text = getTextColor(hex); const secondary = getSettingsValue("colorScheme") === "dark" ? gradient[8] : gradient[2]; const secondaryText = getTextColor(secondary); return i$3` :where(:root), ${r$4(selector)}, .dark, ${r$4(selector)}.dark { --theme-primary-color: ${r$4(hex)};; --theme-primary-text-color: ${r$4(text)};; --theme-secondary-color: ${r$4(secondary)};; --theme-secondary-text-color: ${r$4(secondaryText)};; color-scheme: dark; --theme-body-background: ${r$4(colors.dark["600"])};; --theme-body-text-color: ${r$4(colors.dark["50"])};; --theme-text-color: ${r$4(colors.dark["50"])};; --theme-background-color: ${r$4(colors.dark["600"])};; --theme-hightlight-color: ${r$4(colors.dark["500"])};; --theme-border-color: ${r$4(colors.dark["400"])};; --mov-color-fill-quiet: ${r$4(gradient[9])};; --mov-color-fill-normal: var(--theme-secondary-color, ${r$4(gradient[8])};); --mov-color-fill-loud: var(--theme-primary-color); --mov-color-border-quiet: ${r$4(gradient[8])};; --mov-color-border-normal: ${r$4(gradient[7])};; --mov-color-border-loud: ${r$4(gradient[6])};; --mov-color-on-quiet: ${r$4(gradient[4])};; --mov-color-on-normal: var(--theme-secondary-text-color, ${r$4(gradient[3])};); --mov-color-on-loud: var(--theme-primary-text-color, white); --mov-color-mix-hover: black 8%; --mov-color-mix-active: black 16%; } .light, ${r$4(selector)};.light { color-scheme: light; --theme-body-background: ${r$4(colors.gray["50"])};; --theme-body-text-color: ${r$4(colors.gray["900"])};; --theme-text-color: ${r$4(colors.gray["900"])};; --theme-background-color: ${r$4(colors.gray["50"])};; --theme-hightlight-color: ${r$4(colors.gray["500"])};; --theme-border-color: ${r$4(colors.gray["100"])};; --mov-color-fill-quiet: ${r$4(gradient[0])};; --mov-color-fill-normal: var(--theme-secondary-color, ${r$4(gradient[1])};); --mov-color-fill-loud: var(--theme-primary-color); --mov-color-border-quiet: ${r$4(gradient[1])};; --mov-color-border-normal: ${r$4(gradient[2])};; --mov-color-border-loud: ${r$4(gradient[4])};; --mov-color-on-quiet: ${r$4(gradient[6])};; --mov-color-on-normal: var(--theme-secondary-text-color, ${r$4(gradient[3])};); --mov-color-on-loud: var(--theme-primary-text-color, white); --mov-color-mix-hover: black 10%; --mov-color-mix-active: black 20%; } `; }; //#endregion //#region src/ui/styles/startButton.css?inline var startButton_default = "#StartMOV {\r\n all: revert;\r\n backface-visibility: hidden;\r\n font-size: 32px;\r\n color: #fff;\r\n cursor: pointer;\r\n margin: 0 auto;\r\n padding: 0.5rem 1rem;\r\n text-align: center;\r\n border: none;\r\n border-radius: 10px;\r\n min-height: 50px;\r\n width: 80%;\r\n position: fixed;\r\n right: 0;\r\n left: 0;\r\n bottom: 0;\r\n z-index: 105000;\r\n transition: all 0.4s ease-in-out;\r\n background-size: 300% 100%;\r\n background-image: linear-gradient(to right, #667eea, #764ba2, #6b8dd6, #8e37d7);\r\n box-shadow: 0 4px 15px 0 rgba(116, 79, 168, 0.75);\r\n}\r\n\r\n#StartMOV:hover {\r\n background-position: 100% 0;\r\n transition: all 0.4s ease-in-out;\r\n}\r\n\r\n#StartMOV:focus {\r\n outline: none;\r\n}\r\n\r\n#pagesSliderVal {\r\n display: block;\r\n text-align: center;\r\n margin-top: 15px;\r\n font-weight: bold;\r\n}\r\n"; //#endregion //#region src/ui/Startup.ts /** * This file provides a web component for managing the startup process of the script. * It replaces the old dialog functions with a self-contained Lit component. */ var MovStartup = class MovStartup extends i$1 { constructor(..._args) { super(..._args); this.mangaPages = 0; this.begin = 1; this.timeoutMs = 3e3; this.status = "initial-prompt"; } static { this.styles = [r$4(startButton_default)]; } connectedCallback() { super.connectedCallback(); if (this.status === "initial-prompt") this.timeoutId = window.setTimeout(() => { this.handleStart(); }, this.timeoutMs); } disconnectedCallback() { super.disconnectedCallback(); window.clearTimeout(this.timeoutId); } handleStart() { window.clearTimeout(this.timeoutId); this.dispatchEvent(new CustomEvent("start", { detail: null })); } handleLateStart(begin, end) { this.dispatchEvent(new CustomEvent("start", { detail: { begin, end } })); } handleButtonCLick() { this.status = "late-start-prompt"; } handleDialogClose(e) { e.stopPropagation(); window.clearTimeout(this.timeoutId); this.status = "late-start-button"; } render() { switch (this.status) { case "late-start-button": return this.renderLateStartButton(); case "late-start-prompt": return this.renderLateStartPrompt(); default: return this.renderInitialPrompt(); } } renderInitialPrompt() { return b$1` ${getLocaleString("STARTING")}
${getLocaleString("WAITING")}
Cancel Start Now
`; } renderLateStartButton() { return b$1` `; } renderLateStartPrompt() { this.beginPage ??= this.begin; this.endPage ??= this.mangaPages; const onSliderInput = (e) => { this.beginPage = e.detail.value[0]; this.endPage = e.detail.value[1]; }; return b$1` ${getLocaleString("STARTING")}
${getLocaleString("CHOOSE_BEGINNING")}
[${String(this.beginPage).padStart(3, "0")} , ${String(this.endPage).padStart(3, "0")}]
Close this.handleLateStart(this.beginPage ?? 0, this.endPage ?? this.mangaPages)} style="--mov-color-fill-loud: ${colors.green[700]}; --mov-color-on-loud: white;" > Run
`; } }; __decorate([n$1({ type: Number, reflect: true })], MovStartup.prototype, "mangaPages", void 0); __decorate([n$1({ type: Number, reflect: true })], MovStartup.prototype, "begin", void 0); __decorate([n$1({ type: Number })], MovStartup.prototype, "timeoutMs", void 0); __decorate([n$1({ type: String, reflect: true })], MovStartup.prototype, "status", void 0); __decorate([r$1()], MovStartup.prototype, "beginPage", void 0); __decorate([r$1()], MovStartup.prototype, "endPage", void 0); MovStartup = __decorate([t$1("script-startup")], MovStartup); //#endregion //#region src/utils/cleanup.ts /** * Removes all event listeners from a DOM element by cloning and replacing it. * This is a brute-force approach, but it is the most reliable way to remove all event listeners when you don't have references to the listeners themselves. * Note: This method can have side effects, such as losing the element's state or breaking references to it. * @internal * @param {HTMLElement} element - The element from which to remove event listeners. * @returns {HTMLElement} The new, listener-free element that replaced the original. */ function removeAllEventListeners(element) { if (!element?.parentNode) return element; const newElement = element.cloneNode(true); element.parentNode.replaceChild(newElement, element); return newElement; } /** * Removes all attributes from a DOM element. * @internal * @param {HTMLElement} element - The element from which to remove attributes. */ var removeAttributes = (element) => { element.getAttributeNames().forEach((attr) => { element?.removeAttribute(attr); }); }; /** * Scrubs one or more DOM elements by removing all their attributes and event listeners. * @param {...HTMLElement} elements - A spread array of HTML elements to clean up. */ var cleanUpElement = (...elements) => { elements?.forEach(removeAttributes); elements?.forEach(removeAllEventListeners); }; //#endregion //#region \0@oxc-project+runtime@0.139.0/helpers/esm/taggedTemplateLiteral.js function _taggedTemplateLiteral(e, t) { return t || (t = e.slice(0)), Object.freeze(Object.defineProperties(e, { raw: { value: Object.freeze(t) } })); } //#endregion //#region src/ui/App.ts var _templateObject; var App = class App extends i$1 { constructor(..._args) { super(..._args); this.loadMode = "wait"; } static { this.styles = [i$3``, r$4(styles_default)]; } async start(begin, end) { if (this.manga) { if (!document.documentElement.hasAttribute("mov")) { cleanUpElement(document.documentElement, document.head, document.body); document.documentElement.setAttribute("mov", ""); } window.scrollTo(0, 0); setAppStateValue("manga", { ...this.manga, begin: begin ?? this.manga.begin, pages: end ?? this.manga.pages }); } } /** * LitElement lifecycle hook, called after the component's first render. * It initializes global event listeners and registers the component's `shadowRoot` * in the application state, making it accessible to other parts of the application * that may need to interact with the DOM. */ firstUpdated() { if (this.loadMode === "always") this.start(); events(); loadImages(); } /** * Renders the application's UI. * This includes applying the current theme and rendering the header, reader, * navigation bar, overlay, and all dialog panels. * @returns The rendered template. */ render() { const manga = getAppStateValue("manga"); const dialog = getAppStateValue("dialog"); return b$1`
${manga ? b$1` ${Reader(manga)} ` : b$1(_templateObject || (_templateObject = _taggedTemplateLiteral([ " <\/script-startup>" ])), this.manga?.pages, this.manga?.begin, this.loadMode === "never" ? "late-start-button" : "initial-prompt", (e) => { this.start(e.detail?.begin, e.detail?.end); })} ${dialog ? b$1` setAppStateValue("dialog", null)} > ${dialog.title} ${dialog.content} ${dialog.footer} ` : ""}
`; } }; __decorate([n$1({ type: String, reflect: true })], App.prototype, "loadMode", void 0); __decorate([n$1({ type: Object })], App.prototype, "manga", void 0); App = __decorate([t$1("manga-online-viewer"), (0, import_lib.useStores)(settings, locale, appState)], App); //#endregion //#region src/ui/styles/externalStyle.ts var externalStyle_default = [ "/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */\n\n/* Document\n ========================================================================== */\n\n/**\n * 1. Correct the line height in all browsers.\n * 2. Prevent adjustments of font size after orientation changes in iOS.\n */\n\nhtml {\n line-height: 1.15; /* 1 */\n -webkit-text-size-adjust: 100%; /* 2 */\n}\n\n/* Sections\n ========================================================================== */\n\n/**\n * Remove the margin in all browsers.\n */\n\nbody {\n margin: 0;\n}\n\n/**\n * Render the `main` element consistently in IE.\n */\n\nmain {\n display: block;\n}\n\n/**\n * Correct the font size and margin on `h1` elements within `section` and\n * `article` contexts in Chrome, Firefox, and Safari.\n */\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n/* Grouping content\n ========================================================================== */\n\n/**\n * 1. Add the correct box sizing in Firefox.\n * 2. Show the overflow in Edge and IE.\n */\n\nhr {\n box-sizing: content-box; /* 1 */\n height: 0; /* 1 */\n overflow: visible; /* 2 */\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\npre {\n font-family: monospace, monospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/* Text-level semantics\n ========================================================================== */\n\n/**\n * Remove the gray background on active links in IE 10.\n */\n\na {\n background-color: transparent;\n}\n\n/**\n * 1. Remove the bottom border in Chrome 57-\n * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.\n */\n\nabbr[title] {\n border-bottom: none; /* 1 */\n text-decoration: underline; /* 2 */\n text-decoration: underline dotted; /* 2 */\n}\n\n/**\n * Add the correct font weight in Chrome, Edge, and Safari.\n */\n\nb,\nstrong {\n font-weight: bolder;\n}\n\n/**\n * 1. Correct the inheritance and scaling of font size in all browsers.\n * 2. Correct the odd `em` font sizing in all browsers.\n */\n\ncode,\nkbd,\nsamp {\n font-family: monospace, monospace; /* 1 */\n font-size: 1em; /* 2 */\n}\n\n/**\n * Add the correct font size in all browsers.\n */\n\nsmall {\n font-size: 80%;\n}\n\n/**\n * Prevent `sub` and `sup` elements from affecting the line height in\n * all browsers.\n */\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsub {\n bottom: -0.25em;\n}\n\nsup {\n top: -0.5em;\n}\n\n/* Embedded content\n ========================================================================== */\n\n/**\n * Remove the border on images inside links in IE 10.\n */\n\nimg {\n border-style: none;\n}\n\n/* Forms\n ========================================================================== */\n\n/**\n * 1. Change the font styles in all browsers.\n * 2. Remove the margin in Firefox and Safari.\n */\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n font-family: inherit; /* 1 */\n font-size: 100%; /* 1 */\n line-height: 1.15; /* 1 */\n margin: 0; /* 2 */\n}\n\n/**\n * Show the overflow in IE.\n * 1. Show the overflow in Edge.\n */\n\nbutton,\ninput { /* 1 */\n overflow: visible;\n}\n\n/**\n * Remove the inheritance of text transform in Edge, Firefox, and IE.\n * 1. Remove the inheritance of text transform in Firefox.\n */\n\nbutton,\nselect { /* 1 */\n text-transform: none;\n}\n\n/**\n * Correct the inability to style clickable types in iOS and Safari.\n */\n\nbutton,\n[type=\"button\"],\n[type=\"reset\"],\n[type=\"submit\"] {\n -webkit-appearance: button;\n}\n\n/**\n * Remove the inner border and padding in Firefox.\n */\n\nbutton::-moz-focus-inner,\n[type=\"button\"]::-moz-focus-inner,\n[type=\"reset\"]::-moz-focus-inner,\n[type=\"submit\"]::-moz-focus-inner {\n border-style: none;\n padding: 0;\n}\n\n/**\n * Restore the focus styles unset by the previous rule.\n */\n\nbutton:-moz-focusring,\n[type=\"button\"]:-moz-focusring,\n[type=\"reset\"]:-moz-focusring,\n[type=\"submit\"]:-moz-focusring {\n outline: 1px dotted ButtonText;\n}\n\n/**\n * Correct the padding in Firefox.\n */\n\nfieldset {\n padding: 0.35em 0.75em 0.625em;\n}\n\n/**\n * 1. Correct the text wrapping in Edge and IE.\n * 2. Correct the color inheritance from `fieldset` elements in IE.\n * 3. Remove the padding so developers are not caught out when they zero out\n * `fieldset` elements in all browsers.\n */\n\nlegend {\n box-sizing: border-box; /* 1 */\n color: inherit; /* 2 */\n display: table; /* 1 */\n max-width: 100%; /* 1 */\n padding: 0; /* 3 */\n white-space: normal; /* 1 */\n}\n\n/**\n * Add the correct vertical alignment in Chrome, Firefox, and Opera.\n */\n\nprogress {\n vertical-align: baseline;\n}\n\n/**\n * Remove the default vertical scrollbar in IE 10+.\n */\n\ntextarea {\n overflow: auto;\n}\n\n/**\n * 1. Add the correct box sizing in IE 10.\n * 2. Remove the padding in IE 10.\n */\n\n[type=\"checkbox\"],\n[type=\"radio\"] {\n box-sizing: border-box; /* 1 */\n padding: 0; /* 2 */\n}\n\n/**\n * Correct the cursor style of increment and decrement buttons in Chrome.\n */\n\n[type=\"number\"]::-webkit-inner-spin-button,\n[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n/**\n * 1. Correct the odd appearance in Chrome and Safari.\n * 2. Correct the outline style in Safari.\n */\n\n[type=\"search\"] {\n -webkit-appearance: textfield; /* 1 */\n outline-offset: -2px; /* 2 */\n}\n\n/**\n * Remove the inner padding in Chrome and Safari on macOS.\n */\n\n[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n/**\n * 1. Correct the inability to style clickable types in iOS and Safari.\n * 2. Change font properties to `inherit` in Safari.\n */\n\n::-webkit-file-upload-button {\n -webkit-appearance: button; /* 1 */\n font: inherit; /* 2 */\n}\n\n/* Interactive\n ========================================================================== */\n\n/*\n * Add the correct display in Edge, IE 10+, and Firefox.\n */\n\ndetails {\n display: block;\n}\n\n/*\n * Add the correct display in all browsers.\n */\n\nsummary {\n display: list-item;\n}\n\n/* Misc\n ========================================================================== */\n\n/**\n * Add the correct display in IE 10+.\n */\n\ntemplate {\n display: none;\n}\n\n/**\n * Add the correct display in IE 10.\n */\n\n[hidden] {\n display: none;\n}\n", "/* Make clicks pass-through */\n#nprogress {\n pointer-events: none;\n}\n\n#nprogress .bar {\n background: #29d;\n\n position: fixed;\n z-index: 1031;\n top: 0;\n left: 0;\n\n width: 100%;\n height: 2px;\n}\n\n/* Fancy blur effect */\n#nprogress .peg {\n display: block;\n position: absolute;\n right: 0px;\n width: 100px;\n height: 100%;\n box-shadow: 0 0 10px #29d, 0 0 5px #29d;\n opacity: 1.0;\n\n -webkit-transform: rotate(3deg) translate(0px, -4px);\n -ms-transform: rotate(3deg) translate(0px, -4px);\n transform: rotate(3deg) translate(0px, -4px);\n}\n\n/* Remove these to get rid of the spinner */\n#nprogress .spinner {\n display: block;\n position: fixed;\n z-index: 1031;\n top: 15px;\n right: 15px;\n}\n\n#nprogress .spinner-icon {\n width: 18px;\n height: 18px;\n box-sizing: border-box;\n\n border: solid 2px transparent;\n border-top-color: #29d;\n border-left-color: #29d;\n border-radius: 50%;\n\n -webkit-animation: nprogress-spinner 400ms linear infinite;\n animation: nprogress-spinner 400ms linear infinite;\n}\n\n.nprogress-custom-parent {\n overflow: hidden;\n position: relative;\n}\n\n.nprogress-custom-parent #nprogress .spinner,\n.nprogress-custom-parent #nprogress .bar {\n position: absolute;\n}\n\n@-webkit-keyframes nprogress-spinner {\n 0% { -webkit-transform: rotate(0deg); }\n 100% { -webkit-transform: rotate(360deg); }\n}\n@keyframes nprogress-spinner {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n}\n\n", "#nprogress .bar {\r\n background: #29d;\r\n position: fixed;\r\n z-index: 1031;\r\n top: 0;\r\n left: 0;\r\n width: 100%;\r\n height: 4px;\r\n}\r\n\r\nhtml[mov] body > *:not(manga-online-viewer, #nprogress) {\r\n /* biome-ignore lint/complexity/noImportantStyles: requirement */\r\n display: none !important;\r\n}\r\n\r\nhtml[mov] {\r\n all: unset;\r\n font-size: 16px;\r\n}\r\n" ].join("\n"); //#endregion //#region src/core/main.ts /** * Prepares the page to display the manga viewer. * @param {[ISite, IManga]} siteMangaTuple - A tuple containing the site and manga objects. * @param {ISite} siteMangaTuple[0] - The site configuration object. * @param {IManga} siteMangaTuple[1] - The manga data object. * @returns {Promise} */ async function preparePage([site, manga]) { logScript(`Found Pages: ${manga.pages} in ${site?.name}`); if (!manga.title) manga.title = document.querySelector("title")?.textContent?.trim(); manga.begin = isBookmarked() ?? manga.begin ?? 1; if (manga.before !== void 0) { logScriptVerbose(`Executing Preparation`); await manga.before(manga.begin ?? 0); } document.head.innerHTML += wrapStyle("externals", externalStyle_default); waitWithTimeout(unsafeWindow.customElements.whenDefined("manga-online-viewer"), 1e4).then(() => { const viewer = document.createElement("manga-online-viewer"); viewer.loadMode = site?.start ?? getSettingsValue("loadMode"); viewer.manga = manga; document.body.appendChild(viewer); logScript(`Viewer Ready`, viewer); }).catch((reason) => logScript("Define WebComponent failed", reason)); } /** * Main script entry point. Finds the current site, runs tests, and starts the viewer. * @param {ISite[]} sites - An array of supported site configurations. * @returns {Promise} */ async function start(sites) { logScript(`Starting ${getInfoGM.script.name} ${getInfoGM.script.version} on ${getDevice()} ${getBrowser()} with ${getEngine()}`); logScript(sites.length, "Known Manga Sites:", sites); const foundSites = sites.filter((s) => s.url.test(window.location.href)); logScript(foundSites.length, "Found sites:", foundSites); const sitePromises = foundSites.map(async (site) => { logScript(`Testing site: ${site.name}`); await runSiteTests(site); logScriptVerbose(site.name, "Passed"); const manga = await site.run(); logScriptVerbose("Processed site:", site, manga); if (manga.pages > 0) return [site, manga]; throw new Error(`${site.name} found ${manga.pages} pages`); }); try { const result = await Promise.any(sitePromises); logScriptVerbose("Going with", result[0].name); preparePage(result); } catch (error) { if (error instanceof AggregateError) { logScript("All sites failed to run:"); for (const err of error.errors) logScript(err.message); } else logScript("An unexpected error occurred:", error); } } //#endregion //#region src/utils/find.ts /** * A record of available content matching strategies. * @internal */ var matchers = { eq: (element, content) => element.textContent?.trim() === content, starts: (element, content) => !!element.textContent?.trim()?.startsWith(content), ends: (element, content) => !!element.textContent?.trim()?.endsWith(content) }; /** * Finds all elements that match a given selector and whose text content satisfies a specific matching strategy. * @internal * @param {string} selector - The CSS selector to query for elements. * @param {string | string[]} content - The string or array of strings to match against the elements' text content. * @param {string} matcherKey - The key for the matching strategy to use (e.g., 'eq', 'starts', 'ends'). * @returns {HTMLElement[]} An array of HTML elements that match the criteria. * @throws {Error} If an invalid `matcherKey` is provided. */ function findElements(selector, content, matcherKey) { const matcher = matchers[matcherKey]; if (!matcher) throw new Error(`Invalid matcherKey: ${matcherKey}`); return [...document.querySelectorAll(selector)].filter((element) => _.castArray(content).some((c) => matcher(element, c))); } /** * Finds the first element that matches a given selector and whose text content satisfies a specific matching strategy. * @internal * @param {string} selector - The CSS selector to query for elements. * @param {string | string[]} content - The string or array of strings to match against the elements' text content. * @param {string} matcherKey - The key for the matching strategy to use. * @returns {HTMLElement | undefined} The first matching HTML element, or `undefined` if none is found. */ function findOneElement(selector, content, matcherKey) { return findElements(selector, content, matcherKey)?.[0]; } /** * Finds the first element that matches a content strategy and then finds its closest ancestor matching a given selector. * @internal * @param {string} selector - The CSS selector for the target element. * @param {string | string[]} content - The content to match within the target element. * @param {string} matcherKey - The key for the matching strategy. * @param {string} [ancestor='a'] - The CSS selector for the ancestor to find. * @returns {HTMLElement | null} The closest ancestor element, or `null` if not found. */ function findClosest(selector, content, matcherKey, ancestor = "a") { return findOneElement(selector, content, matcherKey)?.closest(ancestor) ?? null; } /** * Finds all elements that match a selector and have text content exactly equal to the given string(s). * @param {string} selector - The CSS selector. * @param {string | string[]} content - The content to match exactly. * @returns {HTMLElement[]} The matching elements. */ var findByContentEq = (selector, content) => findElements(selector, content, "eq"); /** * Finds an element with exact content and then gets its closest ancestor. * @param {string} selector - The CSS selector for the descendant. * @param {string | string[]} content - The exact content to match. * @param {string} [ancestor='a'] - The selector for the ancestor. * @returns {HTMLElement | null} The matching ancestor element. */ var findClosestByContentEq = (selector, content, ancestor = "a") => findClosest(selector, content, "eq", ancestor); /** * Finds an element with starting content and then gets its closest ancestor. * @param {string} selector - The CSS selector for the descendant. * @param {string | string[]} content - The starting content to match. * @param {string} [ancestor='a'] - The selector for the ancestor. * @returns {HTMLElement | null} The matching ancestor element. */ var findClosestByContentStarts = (selector, content, ancestor = "a") => findClosest(selector, content, "starts", ancestor); /** * Finds an element with ending content and then gets its closest ancestor. * @param {string} selector - The CSS selector for the descendant. * @param {string | string[]} content - The ending content to match. * @param {string} [ancestor='a'] - The selector for the ancestor. * @returns {HTMLElement | null} The matching ancestor element. */ var findClosestByContentEnds = (selector, content, ancestor = "a") => findClosest(selector, content, "ends", ancestor); //#endregion //#region src/main/asura.ts var asura = { name: "Asura Scans", url: /https?:\/\/(www.)?(asurascans|asuracomics).(com|net)\/.+/, homepage: "https://asurascans.com/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "astro-island[component-url*=\"ChapterReader\"]", run() { const island = document.querySelector("astro-island[component-url*=\"ChapterReader\"]"); const props = JSON.parse(island?.getAttribute("props") || "{}"); const seriesSlug = props.seriesSlug?.[1]; const imgUrls = (props.pages?.[1] || []).map((p) => p[1]?.url?.[1]).filter(Boolean); return { title: `${props.seriesName?.[1]} - Chapter ${props.chapterName?.[1]}`, series: `/comics/${seriesSlug}`, pages: imgUrls.length, prev: findClosestByContentEq("span", "Prev", "a")?.getAttribute("href") || document.querySelector("link[rel=\"prev\"]")?.getAttribute("href") || void 0, next: findClosestByContentEq("span", "Next", "a")?.getAttribute("href") || document.querySelector("link[rel=\"next\"]")?.getAttribute("href") || void 0, listImages: imgUrls }; } }; //#endregion //#region src/main/batoto.ts var batoto = { name: "Batoto", url: /https?:\/\/(?:www\.)?(?:fto|jto|hto|dto|mto|wto|bato|battwo|batotwo|comiko|batocomic|readtoto|zbato|xbato|mangatoto)\.(?:to|com|net|org)\/(chapter|title).*/, homepage: "https://rentry.co/batoto", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "div[name=\"image-item\"] img, .page-img", run() { if (window.location.pathname.startsWith("/title")) { if (window.location.search !== "?load=2") window.location.search = "?load=2"; const images = [...document.querySelectorAll("div[name=\"image-item\"] img")]; return { title: document.querySelector("h6")?.textContent?.trim(), series: document.querySelector("h3 a")?.getAttribute("href"), pages: images.length, prev: findClosestByContentEnds("span", "Prev Chapter", "a")?.getAttribute("href"), next: findClosestByContentStarts("span", "Next Chapter", "a")?.getAttribute("href"), listImages: images.map((img) => img.getAttribute("src") ?? "") }; } const images = [...document.querySelectorAll(".page-img")]; return { title: document.querySelector(".nav-title a")?.textContent?.trim(), series: document.querySelector(".nav-title a")?.getAttribute("href"), pages: images.length, prev: document.querySelector(".nav-prev a")?.getAttribute("href"), next: document.querySelector(".nav-next a")?.getAttribute("href"), listImages: images.map((img) => img.getAttribute("src") ?? "") }; } }; //#endregion //#region src/main/bilibilicomics.ts var bilibilicomics = { name: "BilibiliComics", url: /https?:\/\/(www\.)?(bilibilicomics).net\/episode\/.+/, homepage: "https://www.bilibilicomics.net/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "#__NUXT_DATA__", async run() { const images = JSON.parse(document.querySelector("#__NUXT_DATA__")?.innerHTML ?? "").filter((x) => typeof x === "string" && /.(png|jpg|jpeg|gif|bmp|webp)$/i.exec(x)); return { title: document.querySelector(".chapterTitle")?.textContent?.trim(), series: document.querySelector(".book-name")?.getAttribute("href"), pages: images.length, prev: document.querySelectorAll(".pre-next-btns").item(0)?.getAttribute("href"), next: document.querySelectorAll(".pre-next-btns").item(2)?.getAttribute("href"), listImages: images.map((image) => `https://static.comicfans.io/${image}`) }; } }; //#endregion //#region src/main/comick.ts var comick = { name: "Comick", url: /https?:\/\/(www\.)?comick.io\/.+/, homepage: "https://comick.io/", language: [Language.ENGLISH], category: Category.MANGA, waitFunc() { return /\/([^/]+)-chapter.+$/.test(window.location.pathname); }, waitEle: "#__NEXT_DATA__", waitTime: 3e3, run() { const data = JSON.parse(document.getElementById("__NEXT_DATA__")?.innerHTML ?? "")?.props?.pageProps; const pages = data?.chapter?.md_images?.map((image) => `https://meo.comick.pictures/${image?.b2key}`); return { title: data?.seoTitle ?? `${data.chapter?.md_comics?.title} ${data?.chapter?.chap}`, series: `/comic/${data?.chapter?.md_comics?.slug}`, pages: pages?.length, prev: data?.prev?.href, next: data?.next?.href, listImages: pages }; } }; //#endregion //#region src/utils/bruteforce.ts /** * Converts a blob URL to a self-contained data URL. * Very useful for offline stability and portability of blob-based images. * * @param blobUrl - The blob URL to convert. * @returns A promise that resolves to the data URL or the original blob URL on failure. */ async function blobUrlToDataUrl(blobUrl) { try { const blob = await (await fetch(blobUrl)).blob(); return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } catch (e) { console.error(`Failed to convert blob URL ${blobUrl} to data URL:`, e); return blobUrl; } } /** * A brute-force method that scrolls page-by-page sequentially, waiting for each image to load * and descramble before collecting its source. Shows a progress overlay during the process. * * @param expectedPagesCount - The total number of pages to collect. * @param isReaderImage - A predicate to identify if an image element is a reader page. * @param isValidSource - A predicate to check if the image has loaded a valid (descrambled) source. * @returns A promise resolving to an array of collected image URLs (with blob URLs converted to data URLs). */ async function bruteforceScroll(expectedPagesCount, isReaderImage, isValidSource) { const overlay = document.createElement("div"); overlay.style.cssText = "position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(33, 37, 41, 0.95); z-index: 999999; display: flex; flex-direction: column; align-items: center; justify-content: center; color: white; font-family: sans-serif; font-size: 20px;"; overlay.innerHTML = `
MangaOnlineViewer
Preparing pages...
`; document.body.appendChild(overlay); const updateProgress = (collected, total) => { const pct = total > 0 ? Math.round(collected / total * 100) : 0; const textEl = document.getElementById("mov-loading-text"); const barEl = document.getElementById("mov-loading-bar"); if (textEl) textEl.textContent = `Scrolling and waiting for pages to load: ${collected} / ${total || "?"}`; if (barEl) barEl.style.width = total > 0 ? `${pct}%` : "50%"; }; updateProgress(0, expectedPagesCount); const origScrollY = window.scrollY; const collectedUrls = []; try { for (let i = 0; i < expectedPagesCount; i++) { let imgElement = null; let elapsedImage = 0; while (elapsedImage < 5e3) { const readerImages = [...document.querySelectorAll("img")].filter(isReaderImage); if (readerImages[i]) { imgElement = readerImages[i]; break; } await new Promise((resolve) => setTimeout(resolve, 100)); elapsedImage += 100; } if (!imgElement) { logScript(`Failed to find image element for page ${i + 1}`); continue; } imgElement.scrollIntoView({ behavior: "auto", block: "center" }); let elapsedLoad = 0; let validSrc = ""; while (elapsedLoad < 5e3) { const src = imgElement.src || imgElement.getAttribute("src") || ""; if (src && isValidSource(src) && (imgElement.naturalWidth > 250 || imgElement.complete)) { const w = imgElement.naturalWidth || imgElement.width || 0; if (w === 0 || w >= 250) { validSrc = src; break; } } await new Promise((resolve) => setTimeout(resolve, 100)); elapsedLoad += 100; } if (validSrc) collectedUrls.push(validSrc); else { logScript(`Timeout waiting for image ${i + 1} to load`); collectedUrls.push(imgElement.src || imgElement.getAttribute("src") || ""); } updateProgress(collectedUrls.length, expectedPagesCount); } } finally { window.scrollTo(0, origScrollY); overlay.remove(); } if (collectedUrls.length === 0) throw new Error("No images collected from the page"); return await Promise.all(collectedUrls.map(async (url) => { if (url.startsWith("blob:")) return await blobUrlToDataUrl(url); return url; })); } //#endregion //#region src/main/comix.ts /** biome-ignore-all lint/suspicious/noExplicitAny: brute force */ var chapterPagesData = null; if (typeof window !== "undefined" && window.location.hostname.includes("comix.to")) { const originalParse = JSON.parse; JSON.parse = (text, reviver) => { const parsed = originalParse(text, reviver); try { if (parsed?.result?.pages) { chapterPagesData = parsed.result.pages; logScript("Intercepted chapter pages data from JSON.parse", chapterPagesData); } } catch (_e) {} return parsed; }; } function findQueryData() { const root = document.querySelector("#app-root"); if (!root) return null; const fiberKey = Object.keys(root).find((key) => key.startsWith("__reactContainer") || key.startsWith("__reactFiber")); if (!fiberKey) return null; const stack = [root[fiberKey]]; while (stack.length > 0) { const curr = stack.pop(); if (!curr) continue; if (curr.stateNode?.props) { const client = curr.stateNode.props.client; if (client && typeof client.getQueryCache === "function") { const queries = client.getQueryCache().getAll(); for (const q of queries) { const data = q.state.data; if (data?.result?.pages) return data.result.pages; } } } if (curr.memoizedProps) { const client = curr.memoizedProps.client || curr.memoizedProps.value; if (client && typeof client.getQueryCache === "function") { const queries = client.getQueryCache().getAll(); for (const q of queries) { const data = q.state.data; if (data?.result?.pages) return data.result.pages; } } } if (curr.child) stack.push(curr.child); if (curr.sibling) stack.push(curr.sibling); } return null; } function findChapterListFromCache() { const root = document.querySelector("#app-root"); if (!root) return null; const fiberKey = Object.keys(root).find((key) => key.startsWith("__reactContainer") || key.startsWith("__reactFiber")); if (!fiberKey) return null; const stack = [root[fiberKey]]; while (stack.length > 0) { const curr = stack.pop(); if (!curr) continue; const checkClient = (client) => { if (client && typeof client.getQueryCache === "function") { const queries = client.getQueryCache().getAll(); for (const q of queries) { const data = q.state.data; if (data) { if (Array.isArray(data) && data.length > 0 && (data[0].chapterNumber !== void 0 || data[0].number !== void 0)) return data; if (data.result && Array.isArray(data.result.items) && data.result.items.length > 0) { const first = data.result.items[0]; if (first.chapterNumber !== void 0 || first.number !== void 0 || first.mangaId !== void 0) return data.result.items; } } } } return null; }; if (curr.stateNode?.props) { const res = checkClient(curr.stateNode.props.client); if (res) return res; } if (curr.memoizedProps) { const res = checkClient(curr.memoizedProps.client || curr.memoizedProps.value); if (res) return res; } if (curr.child) stack.push(curr.child); if (curr.sibling) stack.push(curr.sibling); } return null; } function getPageCountFromSelect() { const select = document.querySelector("select[aria-label*=\"page\" i], select[class*=\"page\" i], select[id*=\"page\" i]"); if (select) return select.querySelectorAll("option").length; return 0; } function findPageCountFromDOM() { const elements = [...document.querySelectorAll("span, div, button, option")]; for (const el of elements) { const text = el.textContent || ""; const match = /^\s*1\s*(?:\/|of)\s*(\d+)\s*$/i.exec(text.trim()); if (match) { const num = parseInt(match[1], 10); if (num > 0 && num < 500) return num; } } return 0; } function getPrevNextLinks() { const links = [...document.querySelectorAll("a")]; let next = null; let prev = null; for (const a of links) { const href = a.getAttribute("href"); if (!href?.includes("/title/") || !/\/\d+-chapter-/.test(href)) continue; const text = (a.textContent || "").toLowerCase().trim(); const ariaLabel = (a.getAttribute("aria-label") || "").toLowerCase(); if (text.includes("next") || ariaLabel.includes("next") || a.querySelector("[class*=\"next\"]") || a.querySelector("[class*=\"right\"]")) next = href; if (text.includes("prev") || text.includes("previous") || ariaLabel.includes("prev") || ariaLabel.includes("previous") || a.querySelector("[class*=\"prev\"]") || a.querySelector("[class*=\"left\"]")) prev = href; } return { prev, next }; } function getNavigationFromLinks() { const links = [...document.querySelectorAll("a")]; const chapterLinks = []; for (const a of links) { const href = a.getAttribute("href"); if (!href?.includes("/title/") || !/\/\d+-chapter-/.test(href)) continue; const match = /-chapter-(\d+(\.\d+)?)/.exec(href); if (match) chapterLinks.push({ href, num: parseFloat(match[1]) }); } if (chapterLinks.length === 0) return { prev: null, next: null }; const seen = /* @__PURE__ */ new Set(); const uniqueChapters = chapterLinks.filter((item) => { const path = item.href.split("#")[0].split("?")[0]; if (seen.has(path)) return false; seen.add(path); return true; }); uniqueChapters.sort((a, b) => a.num - b.num); const currentCleanPath = window.location.pathname.split("#")[0].split("?")[0]; const idx = uniqueChapters.findIndex((item) => item.href.includes(currentCleanPath) || currentCleanPath.includes(item.href)); let prev = null; let next = null; if (idx !== -1) { if (idx > 0) prev = uniqueChapters[idx - 1].href; if (idx < uniqueChapters.length - 1) next = uniqueChapters[idx + 1].href; } return { prev, next }; } function getPrevNext() { const { prev, next } = getPrevNextLinks(); if (prev || next) return { prev, next }; return getNavigationFromLinks(); } function isReaderImage(img) { if (!img.closest("#app-root") && !img.closest(".rpage-body")) return false; const src = img.src || img.getAttribute("src") || ""; if (!src) return false; if (src.includes("avatar") || src.includes("logo") || src.includes("icon") || src.includes("placeholder")) return false; if (src.startsWith("data:image/svg+xml")) return false; const w = img.naturalWidth || img.width || 0; return !(w > 0 && w < 250); } function isValidSource(src) { if (!src) return false; if (src.includes("placeholder") || src.startsWith("data:image/svg+xml") || src.includes("loading")) return false; return src.startsWith("data:") || src.startsWith("blob:") || src.includes("comix.to") || src.includes("static.comix.to") || src.includes("wowpic"); } var comix = { name: "Comix.to", homepage: "https://comix.to/", url: /https?:\/\/comix\.to\/(title|comic)\/.+\/.+/, language: Language.ENGLISH, category: Category.MANGA, async run() { let elapsed = 0; let pagesData = null; let chapterList = null; while (elapsed < 5e3) { if (!pagesData) pagesData = findQueryData(); if (!chapterList) chapterList = findChapterListFromCache(); if (pagesData && chapterList) break; await new Promise((resolve) => setTimeout(resolve, 100)); elapsed += 100; } if (!pagesData && chapterPagesData) pagesData = chapterPagesData; let expectedPagesCount = pagesData?.items?.length || 0; if (expectedPagesCount === 0) expectedPagesCount = getPageCountFromSelect(); if (expectedPagesCount === 0) expectedPagesCount = findPageCountFromDOM(); if (expectedPagesCount === 0) expectedPagesCount = [...document.querySelectorAll("#app-root img, .rpage-body img")].filter(isReaderImage).length || 40; logScript(`Identified expected page count: ${expectedPagesCount}`); const listImages = await bruteforceScroll(expectedPagesCount, isReaderImage, isValidSource); const initialDataEl = document.getElementById("initial-data"); const initialData = initialDataEl ? JSON.parse(initialDataEl.textContent || "{}") : {}; let title = ""; let series = ""; if (initialData.queries) for (const key of Object.keys(initialData.queries)) { const queryData = initialData.queries[key]; if (queryData?.title && queryData.url) { title = queryData.title; series = queryData.url; break; } } if (!title) title = document.querySelector("title")?.textContent?.trim() || ""; if (!series) series = document.querySelector("a[href^=\"/title/\"]:not([href*=\"-chapter-\"])")?.getAttribute("href") || ""; let prev = null; let next = null; const slugMatch = /\/title\/([^/]+)/.exec(window.location.pathname); const mangaSlug = slugMatch ? slugMatch[1] : ""; const chapterMatch = /\/(\d+)-chapter-/.exec(window.location.pathname); const currentChapterId = chapterMatch ? parseInt(chapterMatch[1], 10) : 0; if (chapterList && currentChapterId && mangaSlug) { const getNum = (ch) => parseFloat(ch.number ?? ch.chapterNumber ?? "0"); const sortedChapters = [...chapterList].sort((a, b) => getNum(a) - getNum(b)); const idx = sortedChapters.findIndex((ch) => parseInt(ch.id, 10) === currentChapterId); if (idx !== -1) { if (idx > 0) { const prevCh = sortedChapters[idx - 1]; const prevNum = prevCh.number ?? prevCh.chapterNumber; prev = `/title/${mangaSlug}/${prevCh.id}-chapter-${prevNum}`; } if (idx < sortedChapters.length - 1) { const nextCh = sortedChapters[idx + 1]; const nextNum = nextCh.number ?? nextCh.chapterNumber; next = `/title/${mangaSlug}/${nextCh.id}-chapter-${nextNum}`; } } } if (!prev && !next) { const nav = getPrevNext(); prev = nav.prev; next = nav.next; } return { title, series, pages: listImages.length, prev, next, listImages }; } }; //#endregion //#region src/main/demonicscans.ts var demonicscans = { name: "DemonicScans", url: /https?:\/\/(www\.)?demonicscans\.org\/(title\/.+\/chapter\/.+|chaptered\.php\?manga=\d+&chapter=.+)/, homepage: "https://demonicscans.org/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "img.imgholder[src*=\"cdn.demoniclibs.com\"]", async run() { const images = [...document.querySelectorAll("img.imgholder[src*=\"cdn.demoniclibs.com\"]")].map((img) => img.getAttribute("data-src") || img.getAttribute("src") || ""); return { title: document.querySelector("title")?.textContent?.trim(), series: document.querySelector("h1 a")?.getAttribute("href"), pages: images.length, prev: document.querySelector(".prevchap")?.getAttribute("href"), next: document.querySelector(".nextchap")?.getAttribute("href"), listImages: images }; } }; //#endregion //#region src/main/dysnatyscans.ts var dynastyscans = { name: "Dynasty-Scans", url: /https?:\/\/(www\.)?dynasty-scans.com\/chapters\/.+/, homepage: "https://dynasty-scans.com/", language: [Language.ENGLISH], category: Category.MANGA, run() { return { title: document.querySelector("#chapter-title")?.textContent?.trim(), series: document.querySelector("#chapter-title a")?.getAttribute("href"), pages: unsafeWindow.pages.length, prev: document.querySelector("#prev_link")?.getAttribute("href"), next: document.querySelector("#next_link")?.getAttribute("href"), listImages: unsafeWindow.pages.map((x) => x.image) }; } }; //#endregion //#region src/main/flamecomics.ts var flamecomics = { name: "Flame Comics", url: /https?:\/\/(www.)?(flamecomics).(xyz)\/series\/.+/, homepage: "https://flamecomics.xyz/", language: [Language.ENGLISH], category: Category.MANGA, run() { const cdn = "https://cdn.flamecomics.xyz/uploads/images/series"; const json = JSON.parse(document.getElementById("__NEXT_DATA__")?.innerHTML ?? ""); const chapter = json?.props?.pageProps?.chapter; const images = _.keys(chapter?.images).map((i) => `${cdn}/${chapter?.series_id}/${chapter?.token}/${chapter?.images?.[i]?.name}?${chapter?.unix_timestamp}`); return { title: `${chapter?.title} ${chapter?.chapter}`, series: `../${chapter?.series_id}`, pages: images.length, prev: json?.props?.pageProps?.previous, next: json?.props?.pageProps?.next, listImages: images }; } }; //#endregion //#region src/main/foolslide.ts var foolslide = { name: ["FoOlSlide", "Kireicake"], url: /^(?!.*jaiminisbox).*\/read\/.+/, homepage: ["https://github.com/saintly2k/FoOlSlideX", "https://reader.kireicake.com"], language: [Language.ENGLISH], obs: "Any Site that uses FoOLSlide", category: Category.MANGA, waitEle: "img.open", run() { const chapter = [...document.querySelectorAll(".topbar_left .dropdown_parent:last-of-type li")]; const origin = chapter.findIndex((item) => { const url = item.querySelector("a")?.getAttribute("href"); if (url) return window.location.href.startsWith(url); return false; }); const pages = [...document.querySelectorAll(".topbar_right .dropdown li")]; const images = [...document.querySelectorAll(".inner img:not(.open)")]; const num = images.length > 1 ? images.length : pages.length; const manga = { title: chapter.at(origin)?.querySelector("a")?.textContent?.trim() ?? document.querySelector("title")?.textContent?.trim(), series: document.querySelector("div.tbtitle div.text a")?.getAttribute("href"), pages: num, prev: chapter.at(origin + 1)?.querySelector("a")?.getAttribute("href"), next: chapter.at(origin - 1)?.querySelector("a")?.getAttribute("href") }; if (images.length > 1) return { ...manga, listImages: images.map((img) => img.getAttribute("src") ?? "") }; return { ...manga, listPages: Array(num).fill(0).map((_, i) => `${window.location.href.replace(/\/\d+$/, "")}/${i + 1}`), img: "img.open" }; } }; //#endregion //#region src/main/ikigai.ts var ikigai = { name: ["Ikigai Mangas - EltaNews", "Ikigai Mangas - Ajaco"], url: /https?:\/\/(visorikigai|visualikigai).(ajaco|eltanews|foodib|jobswu).(com|net|site)\/capitulo\/\d+/, homepage: ["https://visorikigai.eltanews.com/", "https://visorikigai.ajaco.net/"], language: [Language.SPANISH], category: Category.MANGA, run() { const images = document.querySelector("script[type=\"qwik/json\"]")?.textContent?.match(/http[^'"]+webp/gi) ?? []; return { title: document.querySelector("title")?.text.replace(" — Manga en línea | MangaOni", ""), pages: images?.length, prev: findClosestByContentEq("span", "Siguiente")?.getAttribute("href"), next: findClosestByContentEq("span", "Anterior")?.getAttribute("href"), listImages: images }; } }; //#endregion //#region src/main/kagane.ts /** * A utility function to pause execution. * @param {number} ms - Milliseconds to wait. */ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function collectAllSources(num) { const container = document.querySelector(".reader-content"); const srcArray = /* @__PURE__ */ new Set(); const scrollIncrement = 2e3; const maxScrollAttempts = 200; if (!container) { console.error("Error: '.reader-content' container not found."); return []; } let attempts = 0; while (srcArray.size < num && attempts < maxScrollAttempts) { container.scrollTop += scrollIncrement; attempts++; await delay(300); const currentImages = document.querySelectorAll(".reader-page img"); for (const img of currentImages) { const src = img.getAttribute("src"); if (src && src.length > 0) srcArray.add(src); } console.log(`Attempt ${attempts}: Collected ${srcArray.size} of ${num} sources.`); if (container.scrollHeight - container.clientHeight <= container.scrollTop && srcArray.size < num) { console.warn(`Reached end of scrollable content. Stopping with ${srcArray.size} sources.`); break; } } return Array.from(srcArray); } var kagane = { name: "Kagane", homepage: "https://kagane.org/", url: /https:\/\/(www\.)?kagane\.org\/series\/.+\/reader\/.+/, language: Language.ENGLISH, category: Category.MANGA, waitEle: ".reader-page img", async run() { const url = window.location.href; const seriesId = url.match(/series\/([^/]+)/)?.[1]; const chapterId = url.match(/reader\/([^/]+)/)?.[1]; const rschDid = localStorage.getItem("rsch_did"); const headers = {}; if (rschDid) headers["X-Rsch-Did"] = rschDid; const seriesData = await fetch(`https://api.kagane.org/api/v1/series/${seriesId}`, { headers }).then((res) => res.json()); const content = (await fetch(`https://api.kagane.org/api/v1/books/${seriesId}`, { headers }).then((res) => res.json())).data.content; const currentChapter = content.find((c) => c.id === chapterId); const chapterIndex = content.findIndex((c) => c.id === chapterId); const prevChapter = content[chapterIndex + 1]; const nextChapter = content[chapterIndex - 1]; return { title: `${seriesData.data.name} - ${currentChapter?.metadata?.title}`, series: `/series/${seriesId}`, pages: currentChapter?.media?.pagesCount ?? 0, prev: prevChapter ? `/series/${seriesId}/books/${prevChapter.id}` : void 0, next: nextChapter ? `/series/${seriesId}/books/${nextChapter.id}` : void 0, listImages: await collectAllSources(currentChapter?.media?.pagesCount ?? 0) }; } }; //#endregion //#region src/main/kumanga.ts var kumanga = { name: "KuManga", url: /https?:\/\/(www\.)?kumanga.com\/manga\/leer\/.+/, homepage: "https://www.kumanga.com/", language: [Language.SPANISH], category: Category.MANGA, run() { const chapter = document.querySelectorAll("select").item(1).querySelector("option[selected]"); return { title: document.querySelector("title")?.textContent?.trim(), series: document.querySelector("h2 a")?.getAttribute("href"), pages: unsafeWindow.pUrl.length, prev: `/manga/leer/${chapter?.previousElementSibling?.getAttribute("value")}`, next: `/manga/leer/${chapter?.nextElementSibling?.getAttribute("value")}`, listImages: unsafeWindow.pUrl.map((item) => item.imgURL) }; } }; //#endregion //#region src/main/leercapitulo.ts var leercapitulo = { name: "LeerCapitulo", url: /https?:\/\/(www.)?leercapitulo.co\/leer\/.+/, homepage: "https://www.leercapitulo.co/", language: [Language.SPANISH], category: Category.MANGA, waitEle: "#page_select", run() { const img = [...document.querySelectorAll("#page_select option")].map((el) => el.getAttribute("value") ?? ""); return { title: document.querySelector("h1")?.textContent?.trim(), series: document.querySelector(".chapter-title a")?.getAttribute("href"), pages: img.length, prev: document.querySelector(".pre")?.getAttribute("href"), next: document.querySelector(".next")?.getAttribute("href"), listImages: img }; } }; //#endregion //#region src/main/lhtranslation.ts var lhtranslation = { name: "LHTranslation", url: /https?:\/\/(www\.)?lhtranslation.net\/read.+/, homepage: "https://lhtranslation.net/", language: [Language.ENGLISH], category: Category.MANGA, run() { const chapter = document.querySelector(".form-control option:checked"); const images = [...document.querySelectorAll("img.chapter-img")]; return { title: document.querySelector(".chapter-img.tieude font")?.textContent?.trim(), series: document.querySelector(".navbar-brand.manga-name")?.getAttribute("href"), pages: images.length, prev: chapter?.nextElementSibling?.getAttribute("value"), next: chapter?.previousElementSibling?.getAttribute("value"), listImages: images.map((img) => img.getAttribute("src") ?? "") }; } }; //#endregion //#region src/main/localhost.ts var localhost = { name: "Local Files", url: /(file:\/\/\/.+(index)?.html)/, homepage: "/index.html?raw=1", language: [Language.RAW], category: Category.MANGA, run(count) { const num = count ?? parseInt(/\d+/.exec(window.location.search)?.toString() ?? "5", 10); return { title: "Placeholder Manga Loaded", series: "?reload", pages: document.title === "MangaOnlineViewer Script" ? num : 0, begin: 1, prev: "?pages=50", next: "?pages=1", listImages: [ placeholder(1970, 1400, "#2D1657"), placeholder(985, 1400, "#152C55"), placeholder(985, 1400, "#7A1420"), placeholder(1970, 1400, "#806D15"), placeholder(985, 1400, "#0F5B30"), placeholder(1970, 1400, "#1a3e3c"), placeholder(985, 1400, "#480f5b"), placeholder(985, 1400, "#a9bf7a"), placeholder(985, 1400, "#147a56"), placeholder(1970, 1400, "#190343"), placeholder(985, 1400, "#d5b91e"), placeholder(985, 1400, "#836ecd"), placeholder(985, 1400, "#bf19b2"), placeholder(985, 1400, "#152055"), ...Array(num).fill(0).map(randomPlaceholder) ] }; } }; //#endregion //#region src/main/m440.ts var m440 = { name: "M440", url: /https?:\/\/(www\.)?m440.in\/manga\/.+\/.+\/\d+/, homepage: "https://m440.in/", language: [Language.SPANISH], category: Category.MANGA, run() { const images = [...document.querySelectorAll("#all img")]; const chapter = document.querySelector("#chapter-list li.active"); return { title: document.querySelector("title")?.textContent?.trim(), series: document.querySelector("#navbar-collapse-1 ul:nth-child(2) a")?.getAttribute("href"), pages: images.length, prev: chapter?.nextElementSibling?.firstElementChild?.getAttribute("href"), next: chapter?.previousElementSibling?.firstElementChild?.getAttribute("href"), listImages: images.map((img) => img.getAttribute("data-src") ?? "") }; } }; //#endregion //#region src/main/madarawp.ts var imageRegex = /^([\t\n])*(https?:\/\/)?.+\.(jpg|jpeg|png|gif|bmp|webp).*$/; function findImages() { return [...document.querySelectorAll(".wp-manga-chapter-img, .blocks-gallery-item img, .reading-content img, #chapter-images img, #chapterContent img")].map((img) => { const attrs = [...img.attributes].filter((attr) => /.*(src|url).*/i.test(attr.name) && !/^.*(blank|lazy|loading).*$/.test(attr.value)); if (attrs.length === 0) return ""; return attrs.find((attr) => imageRegex.test(attr.value))?.value ?? img?.getAttribute("src") ?? ""; }); } var madarawp = { name: [ "Madara WordPress Plugin", "MangaHaus", "Isekai Scan", "Comic Kiba", "Zinmanga", "mangatx", "Toonily", "Mngazuki", "JaiminisBox", "DisasterScans", "ManhuaPlus", "TopManhua", "NovelMic", "Reset-Scans", "LeviatanScans", "Dragon Tea", "SetsuScans", "ToonGod", "Hades Scans" ], url: /https?:\/\/.+\/(manga|series|manhua|comic|ch|novel|webtoon|tmo)\/.+\/.+/, homepage: [ "https://mangabooth.com/", "https://manhuaus.com", "https://isekaiscan.com/", "https://comickiba.com/", "https://zinmanga.com/", "https://mangatx.com/", "https://toonily.net/", "https://mangazuki.me/", "https://jaiminisbox.net", "https://disasterscans.com/", "https://manhuaplus.org/", "https://www.topmanhua.com/", "https://novelmic.com/", "https://reset-scans.com/", "https://leviatanscans.com/", "https://dragontea.ink/", "https://setsuscans.com/", "https://toongod.org/home/", "https://lectorhades.latamtoon.com" ], language: [Language.ENGLISH], obs: "Any Site that uses Madara WordPress Plugin", category: Category.MANGA, waitFunc: () => { const images = findImages(); return images.length > 0 && images.every((s) => s && imageRegex.test(s)); }, run() { const images = findImages(); return { title: document.querySelector("#chapter-heading")?.textContent?.trim(), series: (document.querySelector(".breadcrumb li:nth-child(3) a") ?? document.querySelector(".breadcrumb li:nth-child(2) a"))?.getAttribute("href"), pages: images.length, prev: document.querySelector(".prev_page")?.getAttribute("href"), next: document.querySelector(".next_page")?.getAttribute("href"), listImages: images }; } }; //#endregion //#region src/main/mangaball.ts var mangaball = { name: "MangaBall", homepage: "https://mangaball.net/", url: /https?:\/\/mangaball\.net\/chapter-detail\/.+/, language: Language.ENGLISH, category: Category.MANGA, run: async () => { const script = [...document.querySelectorAll("script")].find((s) => s.textContent?.includes("chapterImages"))?.textContent; if (!script) return { title: document.querySelector("h1")?.textContent?.trim(), series: document.querySelector("a[href*=\"/title-detail/\"]")?.getAttribute("href") ?? document.querySelector("a[href*=\"/manga-detail/\"]")?.getAttribute("href"), pages: 0, listImages: [] }; const titleId = script.match(/titleId\s*=\s*[`'"](.+?)[`'"]/)?.[1]; const chapterNumber = script.match(/chapterNumber\s*=\s*[`'"](.+?)[`'"]/)?.[1]; const chapterVolume = script.match(/chapterVolume\s*=\s*[`'"](.+?)[`'"]/)?.[1]; const chapterLanguage = script.match(/chapterLanguage\s*=\s*[`'"](.+?)[`'"]/)?.[1]; const images = JSON.parse(script.match(/chapterImages\s*=\s*JSON\.parse\(\s*[`'"](.+?)[`'"]\s*\)/)?.[1] ?? script.match(/chapterImages\s*=.*(\[.*?\])/)?.[1] ?? "[]"); const csrfToken = document.querySelector("meta[name=\"csrf-token\"]")?.getAttribute("content") || ""; const allChapters = ((await (await fetch("/api/v1/chapter/chapter-listing-by-title-id/", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded", "X-CSRF-TOKEN": csrfToken }, body: `title_id=${titleId}&lang=${chapterLanguage}` })).json()).ALL_CHAPTERS || []).sort((a, b) => b.number_float - a.number_float); const findChapter = (direction) => { const currentNumberFloat = parseFloat(chapterNumber || "0"); const currentIndex = allChapters.findIndex((chap) => chap.number_float === currentNumberFloat); if (currentIndex === -1) return void 0; const step = direction === "next" ? -1 : 1; const start = currentIndex + step; const volumeStr = String(chapterVolume); for (let i = start; i >= 0 && i < allChapters.length; i += step) { const trans = allChapters[i].translations.find((t) => t.language === chapterLanguage && String(t.volume) === volumeStr); if (trans) return trans.url; } }; return { title: document.querySelector("h1")?.textContent?.trim(), series: document.querySelector(`a[href*="${titleId}"]`)?.getAttribute("href") ?? document.querySelector("a[href*=\"/title-detail/\"]")?.getAttribute("href") ?? document.querySelector("a[href*=\"/manga-detail/\"]")?.getAttribute("href"), pages: images.length, prev: findChapter("prev"), next: findChapter("next"), listImages: images }; } }; //#endregion //#region src/main/mangabuddy.ts var mangabuddy = { name: "MangaBuddy", url: /https?:\/\/(www\.)?mangabuddy.com\/.+\/chapter.+/, homepage: "https://mangabuddy.com/", language: [Language.ENGLISH], category: Category.MANGA, waitVar: "chapImages", run() { const images = unsafeWindow.chapImages.split(",").map((s) => new URL(s).pathname.replace("/res/", "https://sb.mbcdn.xyz/")); return { title: document.querySelector(".chapter-info")?.textContent?.trim(), series: document.querySelector("#breadcrumbs-container div:nth-child(2) a")?.getAttribute("href"), pages: images.length, prev: document.querySelector("a.prev")?.getAttribute("href"), next: document.querySelector("a.next")?.getAttribute("href"), listImages: images }; } }; //#endregion //#region src/main/mangadex.ts var mangadex = { name: "MangaDex", url: /https?:\/\/(www\.)?mangadex.org/, homepage: "https://mangadex.org/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "#chapter-selector a", async run() { const home = `https://api.mangadex.org/at-home/server/${/\/chapter\/([^/]+)(\/\d+)?/.exec(window.location.pathname)?.at(1)}`; const server = await fetch(home).then(async (res) => res.json()); const images = server.chapter.data; const chapters = document.querySelectorAll("#chapter-selector a"); return { title: document.querySelector("title")?.text.replace(" - MangaDex", ""), series: document.querySelector("a.text-primary[href^='/title/']")?.getAttribute("href"), pages: images.length, prev: chapters?.item(0)?.getAttribute("href"), next: chapters?.item(1)?.getAttribute("href"), listImages: images.map((img) => `${server.baseUrl}/data/${server.chapter.hash}/${img}`) }; } }; //#endregion //#region src/main/mangafox.ts var mangafox = { name: ["MangaFox", "MangaHere"], url: /https?:\/\/(www\.)?(fanfox.net|mangahere.cc)\/manga\/.+\/.+\//, homepage: ["https://fanfox.net/", "https://www.mangahere.cc/"], language: [Language.ENGLISH], category: Category.MANGA, waitVar: "chapterid", async run() { const key = document.querySelector("#dm5_key")?.getAttribute("value"); const options = { method: "GET", headers: { "Content-Type": "text/plain" } }; const src = Array(unsafeWindow.imagecount).fill(0).map(async (_, i) => { const url = `chapterfun.ashx?cid=${unsafeWindow.chapterid ?? unsafeWindow.chapter_id}&page=${i}&key=${key}`; const api = await fetch(url, options).then(async (res) => res.text()); (0, eval)(api); return d; }); const images = await Promise.all(src); return { title: document.querySelector(".reader-header-title div")?.textContent?.trim(), series: document.querySelector(".reader-header-title a")?.getAttribute("href"), pages: unsafeWindow.imagecount, prev: unsafeWindow.prechapterurl, next: unsafeWindow.nextchapterurl, listImages: images.map((img, i) => img[i === 0 ? 0 : 1]) }; } }; //#endregion //#region src/main/mangago.ts var mangago = { name: "Mangago", url: /https?:\/\/(www\.)?mangago.me\/.*\/.*\/.*/, homepage: "https://www.mangago.me/", language: [Language.ENGLISH], category: Category.MANGA, waitVar: "imgsrcs", run() { const key = CryptoJS.enc.Hex.parse("e11adc3949ba59abbe56e057f20f883e"); const opinion = { iv: CryptoJS.enc.Hex.parse("1234567890abcdef1234567890abcdef"), padding: CryptoJS.pad.ZeroPadding }; const images = CryptoJS.AES.decrypt(unsafeWindow.imgsrcs, key, opinion).toString(CryptoJS.enc.Utf8).split(","); return { title: `${unsafeWindow.manga_name} ${unsafeWindow.chapter_name}`, series: unsafeWindow.mid, pages: unsafeWindow.total_pages, prev: document.querySelector(".recom p:nth-child(5) a")?.getAttribute("href"), next: unsafeWindow.next_c_url, listImages: images, before() { if (images.some((s) => s === "")) document.querySelector("#nform")?.submit(); } }; } }; //#endregion //#region src/main/mangahub.ts var mangahub = { name: "MangaHub", url: /https?:\/\/(www\.)?(mangahub).io\/chapter\/.+\/.+/, homepage: "https://mangahub.io/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "#select-chapter", async run() { function getCookie(name) { const value = new RegExp(`${name}=([^;]+)`).exec(document.cookie); return value != null ? decodeURIComponent(value[1]) : null; } const data = { query: `{chapter(x:m01,slug:"${unsafeWindow.CURRENT_MANGA_SLUG ?? window.location.pathname.split("/")[2]}",number:${window.location.pathname.split("/")[3].replace("chapter-", "")}){pages}}` }; const options = { method: "POST", body: JSON.stringify(data), headers: { "Content-Type": "application/json", "x-mhub-access": getCookie("mhub_access") ?? "" } }; const api = await fetch("https://api.mghcdn.com/graphql", options).then(async (res) => res.json()); const images = JSON.parse(api?.data.chapter.pages.toString()); return { title: document.querySelector("#mangareader h3")?.textContent?.trim(), series: document.querySelector("#mangareader a")?.getAttribute("href"), pages: images.i.length, prev: document.querySelector(".previous a")?.getAttribute("href"), next: document.querySelector(".next a")?.getAttribute("href"), listImages: images.i.map((i) => `https://imgx.mghcdn.com/${images.p + i}`) }; } }; //#endregion //#region src/main/mangakakalot.ts var mangakakalot = { name: [ "MangaKakalot", "NeloManga ", "MangaNato", "NatoManga", "MangaBats" ], url: /https?:\/\/(www\.)?(read|chap)?(nelomanga|mangakakalot|natomanga|manganato|mangabats|mangakakalove).(com|gg|net).*\/(chapter|manga)\/.+\/.+/, homepage: [ "https://mangakakalot.gg/", "https://www.nelomanga.com/", "https://www.manganato.gg/", "https://www.natomanga.com/", "https://www.mangabats.com/" ], language: [Language.ENGLISH], category: Category.MANGA, waitEle: ".navi-change-chapter-btn-prev, .next, .navi-change-chapter-btn-next, .back", run() { const images = [...document.querySelectorAll("#vungdoc img, .container-chapter-reader img")]; return { title: document.querySelector(".info-top-chapter h2, .imageOptions-chapter-info-top h1, .panel-chapter-info-top h1")?.textContent?.trim(), series: document.querySelectorAll("span a[title]").item(1).getAttribute("href"), pages: images.length, prev: document.querySelector(".navi-change-chapter-btn-prev, .next")?.getAttribute("href"), next: document.querySelector(".navi-change-chapter-btn-next, .back")?.getAttribute("href"), listImages: images.map((img) => img.getAttribute("src") ?? "") }; } }; //#endregion //#region src/main/mangaoni.ts var mangaoni = { name: "MangaOni", url: /https?:\/\/(www\.)?manga-oni.com\/lector\/.+\/\d+\/cascada/, homepage: "https://manga-oni.com/", language: [Language.SPANISH], category: Category.MANGA, run() { document.querySelector("#c_list")?.dispatchEvent(new Event("mouseover")); const chapter = document.querySelector("#c_list option:checked"); const images = [...document.querySelectorAll("#slider img")]; return { title: document.querySelector("title")?.text.replace(" — Manga en línea | MangaOni", ""), pages: images?.length, prev: chapter?.nextElementSibling?.getAttribute("value"), next: chapter?.previousElementSibling?.getAttribute("value"), listImages: images.map((img) => img.getAttribute("data-src") ?? img.getAttribute("src") ?? "") }; } }; //#endregion //#region src/main/mangapark.ts var mangapark = { name: "MangaPark", url: /https?:\/\/(www\.)?(mangapark|mpark|comicpark|readpark|parkmanga).(com|me|org|net|io|to)\/title\/.+\/.+/, homepage: "https://mangapark.net/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "main div div a.btn-primary", run() { const images = [...document.querySelectorAll("main div div > img.w-full")]; return { title: [...document.querySelectorAll(".comic-detail h3 a, .comic-detail h6 span")].map((e) => e.textContent?.trim()).join(" "), series: document.querySelector(".comic-detail a")?.getAttribute("href"), pages: images.length, prev: document.querySelectorAll("main div div a.btn-primary")?.item(0)?.getAttribute("href"), next: document.querySelectorAll("main div div a.btn-primary")?.item(1)?.getAttribute("href"), listImages: images.map((src) => src.getAttribute("src") ?? "") }; } }; //#endregion //#region src/main/mangareader.ts var mangareader = { name: "MangaReader", url: /https?:\/\/(www\.)?mangareader.to\/read\/.+\/.+\/.+/, homepage: "https://mangareader.to", language: [Language.ENGLISH], category: Category.MANGA, obs: "Some galleries will not be usable", waitEle: ".ds-image, .iv-card", async run() { const chapter = document.querySelector(".chapter-item.active"); const src = [...document.querySelectorAll(".ds-image[data-url], .iv-card[data-url]")].map(async (img) => { const url = img.getAttribute("data-url") ?? ""; if (url && img.classList.contains("shuffled")) return (await imgReverser(url)).toDataURL(); return url; }); return { title: document.querySelector(".hr-manga h2")?.textContent?.trim(), series: document.querySelector(".hr-manga")?.getAttribute("href"), pages: src.length, prev: chapter?.nextElementSibling?.querySelector("a")?.getAttribute("href"), next: chapter?.previousElementSibling?.querySelector("a")?.getAttribute("href"), listImages: await Promise.all(src) }; } }; //#endregion //#region src/main/mangastreamwp.ts var mangastreamwp = { name: [ "MangaStream WordPress Plugin", "Realm Oasis", "Voids-Scans", "Luminous Scans", "Shimada Scans", "Night Scans", "Manhwa-Freak", "OzulScansEn", "CypherScans", "MangaGalaxy", "LuaScans", "Drake Scans", "Rizzfables", "NovatoScans", "TresDaos", "Lectormiau", "NTRGod", "Threedaos" ], url: /https?:\/\/[^/]*(scans?|comic|realm|rizz|hivetoon|tresdaos|zonamiau|ntrgod|threedaos)[^/]*\/.+/, homepage: [ "https://themesia.com/mangastream-wordpress-theme/", "https://realmoasis.com/", "https://void-scans.com/", "https://luminous-scans.com/", "https://shimadascans.com/", "https://night-scans.com/", "https://freakcomic.com/", "https://ozulscansen.com/", "https://cypherscans.xyz/", "https://mangagalaxy.me/", "https://luascans.com/", "https://drake-scans.com/", "https://rizzfables.com/", "https://www.novatoscans.top/", "https://tresdaos.com", "https://zonamiau.com/", "https://ntrgod.com/", "https://threedaos.zdrz.xyz/" ], language: [Language.ENGLISH, Language.SPANISH], category: Category.MANGA, waitEle: ":where(#readerarea, .check-box, #viewer-img) img:not(.asurascans):not([src*=\"loader\"]):not([src*=\"chevron\"]),:where(.nextprev, .inner_nPL)", run() { const images = [...document.querySelectorAll(this.waitEle ?? "")]; return { title: document.querySelector("title")?.textContent?.trim(), series: document.querySelector(":where(.allc, .tac) a")?.getAttribute("href") ?? document.querySelectorAll("[class*=\"breadcrumb\"] a").item(1)?.getAttribute("href"), pages: images.length, prev: findByContentEq(":where(.nextprev, .inner_nPL) a", ["Prev", "Anterior"])?.[0]?.getAttribute("href"), next: findByContentEq(":where(.nextprev, .inner_nPL) a", ["Next", "Siguiente"])?.[0]?.getAttribute("href"), listImages: images.map((img) => img.getAttribute("data-src") ?? img.getAttribute("data-lazy-src") ?? img.getAttribute("src") ?? "") }; } }; //#endregion //#region src/main/mangatoon.ts var mangatoon = { name: "MangaToons", url: /https?:\/\/.*mangatoon.mobi\/.+\/watch\/.+/, homepage: "https://mangatoon.mobi/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: ".pictures img:not(.cover)", run() { const images = [...document.querySelectorAll(".pictures img:not(.cover)")]; return { title: document.querySelector("title")?.textContent?.trim(), series: document.querySelector(".top-left a")?.getAttribute("href"), pages: images.length, prev: document.querySelector(".page-icons-prev")?.getAttribute("href"), next: document.querySelector(".page-icons-next")?.getAttribute("href"), listImages: images.map((img) => img.getAttribute("data-src") ?? "") }; } }; //#endregion //#region src/main/mangatown.ts var mangatown = { name: "MangaTown", url: /https?:\/\/www\.mangatown\.com\/manga\/.+\/.+\//, homepage: "https://www.mangatown.com/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "div.page_select select option, .read_img .image", run() { const options = document.querySelector("div.page_select select")?.querySelectorAll("option"); const images = [...document.querySelectorAll(".read_img .image")].map((img) => img.getAttribute("src") ?? ""); const pages = [...options ?? []].map((element) => element.value).filter((url) => !url.endsWith("featured.html")).map((url) => new URL(url, window.location.href).href); return { title: document.querySelector("div.title h1")?.textContent?.trim(), series: document.querySelector("div.title a")?.getAttribute("href"), pages: unsafeWindow.total_pages || pages.length, prev: document.querySelector(".chapter_select option:checked")?.previousElementSibling?.getAttribute("value"), next: document.querySelector(".chapter_select option:checked")?.nextElementSibling?.getAttribute("value"), ...images.length > 1 ? { listImages: images } : { listPages: pages, img: "#image" } }; } }; //#endregion //#region src/main/manhwaweb.ts var manhwaweb = { name: "ManhwaWeb", url: /https?:\/\/(www\.)?manhwaweb.com\/leer\/.+/, homepage: "https://manhwaweb.com/", language: [Language.SPANISH], category: Category.MANGA, async run() { const slug = window.location.pathname.replace("/leer", ""); const api = await fetch(`https://manhwawebbackend-production.up.railway.app/chapters/see${slug}`).then(async (res) => res.json()); const data = await fetch(`https://manhwawebbackend-production.up.railway.app/chapters/seeprevpost${slug}`).then(async (res) => res.json()); return { title: `${api.name} ${api.chapter.chapter}`, series: [...document.querySelectorAll("div")].filter((i) => i.textContent === "Episodios")?.[0]?.parentElement?.getAttribute("href"), pages: api.chapter.img.length, prev: data.chapterAnterior.replace(api._id, api.real_id), next: data.chapterSiguiente.replace(api._id, api.real_id), listImages: api.chapter.img }; } }; //#endregion //#region src/main/mgeko.ts var mgeko = { name: ["MangaGeko.com", "MangaGeko.cc"], url: /https?:\/\/(www\.)?mgeko.(com|cc)?\/reader\/.*/, homepage: ["https://www.mgeko.com/", "https://www.mgeko.cc/"], language: [Language.ENGLISH], category: Category.MANGA, run() { const images = [...document.querySelectorAll("img[id^=\"image-\"]")]; return { title: document.querySelector(".titles")?.textContent?.trim(), series: document.querySelector(".titles a")?.getAttribute("href"), pages: images.length, prev: document.querySelector(".chnav.prev:not(.isDisabled)")?.getAttribute("href"), next: document.querySelector(".chnav.next:not(.isDisabled)")?.getAttribute("href"), listImages: images.map((img) => img.getAttribute("src") ?? "") }; } }; //#endregion //#region src/main/nineanime.ts var nineanime = { name: "NineAnime", url: /https?:\/\/(www\.)?nineanime.com\/chapter\/.+/, homepage: "https://www.nineanime.com/", language: [Language.ENGLISH], category: Category.MANGA, run() { const pages = [...document.querySelectorAll(".sl-page option")]; const chapter = document.querySelector(".mangaread-pagenav select option[selected]"); return { title: `${document.querySelector(".title h1")?.textContent?.trim()}/${document.querySelector(".title h2")?.textContent?.trim()}`, series: document.querySelector(".title a:has(h2)")?.getAttribute("href"), pages: pages.length, prev: chapter?.nextElementSibling?.getAttribute("value"), next: chapter?.previousElementSibling?.getAttribute("value"), listPages: pages.map((o) => o.getAttribute("value") ?? ""), img: ".manga_pic" }; } }; //#endregion //#region src/main/olympus.ts var olympus = { name: "Olympus", url: /https?:\/\/(www\.)?olympusxyz.com\/capitulo\/\d+\/.+/, homepage: "https://olympusxyz.com/", language: [Language.SPANISH], category: Category.MANGA, run() { const images = [...document.querySelectorAll("section img.w-full.h-full")]; return { title: document.querySelector("title")?.textContent?.replace(/\|.+/, "").trim(), series: document.querySelector("h1")?.closest("a")?.getAttribute("href"), pages: images.length, prev: document.querySelector("a[name=\"capitulo anterior\"]")?.getAttribute("href"), next: document.querySelector("a[name=\"capitulo siguiente\"]")?.getAttribute("href"), listImages: images.map((img) => img.getAttribute("src") ?? "") }; } }; //#endregion //#region src/main/qimanhwa.ts var qimanhwa = { name: "QiManhwa", url: /https?:\/\/qimanhwa\.com\/series\/[^/]+\/chapter-.+/, homepage: "https://qimanhwa.com/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "img[alt*=\"Chapter\"]", run() { const images = [...document.querySelectorAll("img[alt*=\"Chapter\"]")]; return { title: document.querySelector("h1")?.textContent?.trim(), series: findClosestByContentEq("button", "Home", "a")?.getAttribute("href"), pages: images.length, prev: document.querySelector(".lucide-chevron-left")?.closest("a")?.getAttribute("href"), next: document.querySelector(".lucide-chevron-right")?.closest("a")?.getAttribute("href"), listImages: images.map((img) => img.getAttribute("src") ?? "") }; } }; //#endregion //#region src/main/readcomicsonline.ts var readcomicsonline = { name: "ReadComicsOnline", url: /https?:\/\/(www\.)?readcomicsonline.ru\/comic\/.*\/\d*/, homepage: "https://readcomicsonline.ru/", language: [Language.ENGLISH], category: Category.COMIC, run() { const images = [...document.querySelectorAll("#all img")]; return { title: unsafeWindow.title.replace(/ - Page \d+/, ""), series: document.querySelector("div.pager-cnt a")?.getAttribute("href"), pages: unsafeWindow.pages.length, prev: unsafeWindow.prev_chapter, next: unsafeWindow.next_chapter, listImages: images.map((img) => img.getAttribute("data-src") ?? "") }; } }; //#endregion //#region src/main/reaperscans.ts var reaperscans = { name: "ReaperScans", url: /https?:\/\/(www\.)?reaperscans\.com\/series\/.+\/chapter.+/, homepage: "https://reaperscans.com/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "#content .container img:not(.rounded)", run() { const images = [...document.querySelectorAll("#content .container img:not(.rounded)")]; return { title: document.querySelector("title")?.textContent?.trim(), series: document.querySelector("button .fa-house")?.closest("a")?.getAttribute("href"), pages: images.length, prev: document.querySelector(".fa-chevron-left")?.closest("a")?.getAttribute("href"), next: document.querySelector(".fa-chevron-right")?.closest("a")?.getAttribute("href"), listImages: images.map((img) => (img.getAttribute("data-src") || img.getAttribute("src")) ?? "") }; } }; //#endregion //#region src/main/tmofans.ts var tmofans = { name: "TuMangaOnline", url: /https?:\/\/(www\.)?zonatmo\.(com|org|app)\/(viewer|news|view_uploads|reader|library)\/.+/, homepage: "https://zonatmo.org/", language: [Language.SPANISH], category: Category.MANGA, run() { const images = [...document.querySelectorAll(".img-container img, .viewer-container img, .content-image, .viewer-image, .img-fluid, .reader-img-wrap img, .viewer-img, #viewer-container img, .viewer-page")]; const pages = [...document.querySelectorAll("div.container:nth-child(4) select#viewer-pages-select option, #viewer-pages-select option, select#chapter-pages option, select#pages option")]; const num = images.length > 1 ? images.length : pages.length; return { title: document.querySelector("title")?.textContent?.trim(), series: (document.querySelector("a[title=\"Volver\"]") ?? document.querySelector(".breadcrumb-item:nth-child(2) a") ?? document.querySelector(".book-name a") ?? document.querySelector(".breadcrumb-item a"))?.getAttribute("href"), pages: num || 1, prev: (document.querySelector(".chapter-prev a") ?? document.querySelector(".prev_page") ?? document.querySelector("a.prev-chapter") ?? document.querySelector(".chapter-prev-btn"))?.getAttribute("href"), next: (document.querySelector(".chapter-next a") ?? document.querySelector(".next_page") ?? document.querySelector("a.next-chapter") ?? document.querySelector(".chapter-next-btn"))?.getAttribute("href"), ...images.length <= 1 && pages.length > 1 ? { listPages: Array(pages.length).fill(0).map((_, i) => `${window.location.href.replace(/\/\d+$/, "")}/${i + 1}`) } : { listImages: images.map((item) => item.getAttribute("data-src") ?? item.getAttribute("data-original") ?? item.getAttribute("src") ?? "") }, img: "#viewer-container img, .viewer-page, .img-container img, .content-image, .viewer-image, .reader-img-wrap img, .viewer-img", before() { if (window.location.pathname.includes("paginated")) window.location.pathname = window.location.pathname.replace(/paginated.*/, "cascade"); if (window.location.pathname.includes("view_uploads")) { const btn = document.querySelector("a.btn.btn-primary, .btn-primary a, a.btn-block"); if (btn) btn.click(); } } }; } }; //#endregion //#region src/main/vortexscans.ts var vortexscans = { name: "Vortex Scans", url: /https?:\/\/(www.)?(vortexscans).(org)\/.+/, homepage: "https://vortexscans.org/", language: [Language.ENGLISH], category: Category.MANGA, waitVar: "__next_f", waitFunc() { return unsafeWindow.__next_f.find((i) => /images/.test(i?.[1]))?.length > 0; }, run() { const data = unsafeWindow.__next_f.find((i) => /images/.test(i?.[1]))?.[1]; const images = data.slice(data.indexOf("images")).match(/http[^"]+\.(png|gif|jpg|jpeg|webp)/g) ?? []; return { title: document.querySelector("time")?.closest("div")?.querySelector("div")?.textContent?.trim(), series: document.querySelector("time")?.closest("a")?.getAttribute("href"), pages: images?.length, prev: findClosestByContentEq("button", "Prev", "a")?.getAttribute("href"), next: findClosestByContentEq("button", "Next", "a")?.getAttribute("href"), listImages: images }; } }; //#endregion //#region src/userscript-main.ts /** * @file Entry point for the main (SFW) Manga Online Viewer userscript. * This script imports the list of SFW (Safe for Work) websites and initializes the main * application logic to run on them. */ start([ asura, batoto, bilibilicomics, comick, comix, demonicscans, dynastyscans, flamecomics, ikigai, kagane, kumanga, leercapitulo, lhtranslation, localhost, m440, mangabuddy, mangadex, mangafox, mangago, mangahub, mangakakalot, mangaball, mangaoni, mangapark, mangareader, mangatoon, mangatown, manhwaweb, mgeko, nineanime, olympus, qimanhwa, readcomicsonline, reaperscans, tmofans, { name: "WebNovel", url: /https?:\/\/(www\.)?webnovel.com\/comic\/.+/, homepage: "https://www.webnovel.com/", language: [Language.ENGLISH], category: Category.MANGA, waitVar: "g_data", run() { const images = unsafeWindow.g_data.chapter.chapterInfo.chapterPage.map((img) => img.url); return { title: document.querySelector("title")?.textContent?.trim(), series: "./", pages: images.length, prev: `${unsafeWindow.g_data.chapter.chapterInfo.preChapterName}_${unsafeWindow.g_data.chapter.chapterInfo.preChapterId}`, next: `${unsafeWindow.g_data.chapter.chapterInfo.nextChapterName}_${unsafeWindow.g_data.chapter.chapterInfo.nextChapterId}`, listImages: images }; } }, { name: "WebToons", url: /https?:\/\/(www\.)?webtoons.com\/.+viewer.+/, homepage: "https://www.webtoons.com/", language: [Language.ENGLISH], category: Category.MANGA, run() { const images = [...document.querySelectorAll("#_imageList img")]; return { title: document.querySelector(".subj_info")?.textContent?.trim(), series: document.querySelector(".subj_info a")?.getAttribute("href"), pages: images.length, prev: document.querySelector("._prevEpisode")?.getAttribute("href"), next: document.querySelector("._nextEpisode")?.getAttribute("href"), listImages: images.map((img) => img.getAttribute("data-url") ?? img.getAttribute("data-src") ?? img.getAttribute("src") ?? "") }; } }, { name: "WeebCentral", url: /https?:\/\/(www\.)?(weebcentral).com\/chapters\/.+/, homepage: "https://weebcentral.com/", language: [Language.ENGLISH], category: Category.MANGA, waitEle: "section[hx-get*=\"/images\"]", async run() { if (document.documentElement.hasAttribute("mov")) return { pages: 0, listImages: [] }; const imagesUrlBase = document.querySelector("section[hx-get*=\"/images\"]")?.getAttribute("hx-get"); if (!imagesUrlBase) throw new Error("Images HTMX endpoint not found"); const imagesUrl = `${imagesUrlBase.replace(/&/g, "&")}&reading_style=long_strip`; const imagesHtml = await fetch(imagesUrl, { headers: { "HX-Request": "true" } }).then((res) => res.text()); const parser = new DOMParser(); const src = [...parser.parseFromString(imagesHtml, "text/html").querySelectorAll("img")].map((img) => img.getAttribute("src") || img.getAttribute("data-src") || "").filter((s) => s && !s.includes("broken_image")).map((s) => s.startsWith("http") ? s : new URL(s, window.location.origin).href); const uniqueImages = [...new Set(src)].sort((a, b) => { const ma = a.match(/-(\d+)\.[^.]+$/); const mb = b.match(/-(\d+)\.[^.]+$/); return (ma ? parseInt(ma[1], 10) : 0) - (mb ? parseInt(mb[1], 10) : 0); }); const chapterSelectUrl = document.querySelector("button[hx-get*=\"chapter-select\"]")?.getAttribute("hx-get"); const chaptersList = await fetch(chapterSelectUrl ?? "", { headers: { "HX-Request": "true" } }).then((res) => res.text()); const selectedChapter = parser.parseFromString(chaptersList, "text/html").querySelector("#selected_chapter"); const getAbsUrl = (path) => path ? path.startsWith("http") ? path : new URL(path, window.location.origin).href : null; return { title: document.title.split(" - ")[0].trim(), series: getAbsUrl(document.querySelector("main section a.btn-ghost")?.getAttribute("href")), pages: uniqueImages.length, prev: getAbsUrl(selectedChapter?.nextElementSibling?.getAttribute("href")), next: getAbsUrl(selectedChapter?.previousElementSibling?.getAttribute("href")), listImages: uniqueImages, fetchOptions: { headers: { "HX-Request": "true", Referer: window.location.href } } }; } }, { name: "WeebDex", url: /https?:\/\/(www\.)?weebdex\.org\/.+/, homepage: "https://weebdex.org/", language: Language.ENGLISH, category: Category.MANGA, waitEle: "a[href^=\"/title/\"]", async run() { const chapterId = /\/chapter\/([^/]+)/.exec(window.location.pathname)?.at(1); const chapterApi = `https://api.weebdex.org/chapter/${chapterId}`; const chapterData = await fetch(chapterApi).then(async (res) => res.json()); const mangaLink = document.querySelector("a[href^=\"/title/\"]"); const chaptersApi = `https://api.weebdex.org/manga/${chapterData.relationships.manga.id}/aggregate?lang=${chapterData.language}`; const chapters = (await fetch(chaptersApi).then(async (res) => res.json())).chapters || []; const currentIndex = chapters.findIndex((c) => c.chapter === chapterData.chapter); return { title: document.querySelector("title")?.textContent?.trim().replace(/Page \d+:/, ""), series: mangaLink?.getAttribute("href"), pages: chapterData.data.length, prev: chapters[currentIndex + 1] ? `/chapter/${_.keys(chapters[currentIndex + 1].entries)[0]}` : void 0, next: chapters[currentIndex - 1] ? `/chapter/${_.keys(chapters[currentIndex - 1].entries)[0]}` : void 0, listImages: chapterData.data.map((img) => `${chapterData.node}/data/${chapterId}/${img.name}`) }; } }, vortexscans, { name: "ZeroScans", url: /https?:\/\/(www\.)?zscans.com\/comics\/.+/, homepage: "https://zscans.com/", language: [Language.ENGLISH], category: Category.MANGA, waitVar: "__ZEROSCANS__", run() { const images = unsafeWindow.__ZEROSCANS__.data.at(0).current_chapter.high_quality; const chapters = document.querySelectorAll(".v-btn--router"); return { title: document.querySelector("title")?.textContent?.trim(), series: document.querySelector(".v-breadcrumbs li:nth-child(2) + a")?.getAttribute("href"), pages: images.length, prev: chapters[0]?.getAttribute("href"), next: chapters[1]?.getAttribute("href"), listImages: images }; } }, mangastreamwp, foolslide, madarawp ]).catch(logScript); //#endregion })();