- // ==UserScript==
- // @name Thread Media Viewer
- // @description Comfy media browser and viewer for various discussion boards.
- // @version 2.2.0
- // @namespace qimasho
- // @source https://github.com/qimasho/thread-media-viewer
- // @supportURL https://github.com/qimasho/thread-media-viewer/issues
- // @match https://boards.4chan.org/*
- // @match https://boards.4channel.org/*
- // @match https://thebarchive.com/*
- // @require https://cdn.jsdelivr.net/npm/preact@10.4.6/dist/preact.min.js
- // @require https://cdn.jsdelivr.net/npm/preact@10.4.6/hooks/dist/hooks.umd.js
- // @grant GM_addStyle
- // @grant GM_openInTab
- // @license MIT
- // ==/UserScript==
-
- (() => {
- // src/lib/utils.ts
- function isOfType(value, condition) {
- return condition;
- }
- const ns = (name) => `_tmv_${name}`;
- function clamp(min5, value, max5) {
- return Math.max(min5, Math.min(max5, value));
- }
- function withValue(callback) {
- return (event) => {
- const target = event.target;
- if (isOfType(target, target && (target.nodeName === "INPUT" || target.nodeName === "BUTTON"))) {
- callback(target.value);
- }
- };
- }
- function getBoundingDocumentRect(element) {
- const {width, height, top, left, bottom, right} = element.getBoundingClientRect();
- return {
- width,
- height,
- top: window.scrollY + top,
- left: window.scrollX + left,
- bottom: window.scrollY + bottom,
- right: window.scrollX + right
- };
- }
- function scrollToView(element, {
- block = "start",
- behavior = "auto"
- } = {}) {
- if (!document.body.contains(element))
- return;
- let container = element.parentElement;
- while (container) {
- if (isScrollableY(container))
- break;
- else
- container = container.parentElement;
- }
- if (!container)
- return;
- const containerRect = getBoundingDocumentRect(container);
- const elementRect = getBoundingDocumentRect(element);
- const topOffset = elementRect.top - containerRect.top + (container === document.scrollingElement ? 0 : container.scrollTop);
- let requestedOffset;
- if (block === "start")
- requestedOffset = topOffset;
- else if (block === "center")
- requestedOffset = topOffset - container.clientHeight / 2 + element.offsetHeight / 2;
- else if (block === "end")
- requestedOffset = topOffset - container.clientHeight + element.offsetHeight;
- else
- requestedOffset = topOffset - block;
- container.scrollTo({top: requestedOffset, behavior});
- }
- function isScrollableY(element) {
- if (element.scrollHeight === element.clientHeight)
- return false;
- if (getComputedStyle(element).overflowY === "hidden")
- return false;
- if (element.scrollTop > 0)
- return true;
- element.scrollTop = 1;
- if (element.scrollTop > 0) {
- element.scrollTop = 0;
- return true;
- }
- return false;
- }
- function formatSeconds(seconds) {
- let minutes = Math.floor(seconds / 60);
- let leftover = Math.round(seconds - minutes * 60);
- return `${String(minutes).padStart(2, "0")}:${String(leftover).padStart(2, "0")}`;
- }
- function throttle(fn, timeout = 100, noTrailing = false) {
- let timeoutID;
- let args;
- let context;
- let last = 0;
- function call() {
- fn.apply(context, args);
- last = Date.now();
- timeoutID = context = args = null;
- }
- function throttled() {
- let delta = Date.now() - last;
- context = this;
- args = arguments;
- if (delta >= timeout) {
- throttled.cancel();
- call();
- } else if (!noTrailing && timeoutID == null) {
- timeoutID = setTimeout(call, timeout - delta);
- }
- }
- throttled.cancel = () => {
- if (timeoutID !== null) {
- clearTimeout(timeoutID);
- timeoutID = null;
- }
- };
- throttled.flush = () => {
- if (timeoutID !== null) {
- clearTimeout(timeoutID);
- timeoutID = null;
- call();
- }
- };
- return throttled;
- }
- function keyEventId(event) {
- let key = String(event.key);
- const keyAsNumber = Number(event.key);
- const isNumpadKey = event.code.indexOf("Numpad") === 0;
- const isNumpadNumber = keyAsNumber >= 0 && keyAsNumber <= 9 && isNumpadKey;
- if (key === " " || isNumpadNumber)
- key = event.code;
- let id = "";
- if (event.altKey)
- id += "Alt";
- if (event.ctrlKey)
- id += id.length > 0 ? "+Ctrl" : "Ctrl";
- if (event.shiftKey && (key.length > 1 || isNumpadKey))
- id += id.length > 0 ? "+Shift" : "Shift";
- if (key !== "Alt" && key !== "Ctrl" && key !== "Shift")
- id += (id.length > 0 ? "+" : "") + key;
- return id;
- }
-
- // src/lib/syncedSettings.ts
- function syncedSettings(localStorageKey, defaults) {
- const listeners = new Set();
- let savingPromise = null;
- let settings11 = load();
- function triggerListeners() {
- for (let callback of listeners)
- callback(settings11);
- }
- function load() {
- let json = localStorage.getItem(localStorageKey);
- let data;
- try {
- data = json ? {...defaults, ...JSON.parse(json)} : {...defaults};
- } catch (error) {
- data = {...defaults};
- }
- return data;
- }
- function save() {
- if (savingPromise)
- return savingPromise;
- savingPromise = new Promise((resolve) => setTimeout(() => {
- localStorage.setItem(localStorageKey, JSON.stringify(settings11));
- savingPromise = null;
- resolve();
- }, 10));
- return savingPromise;
- }
- window.addEventListener("storage", throttle(() => {
- let newData = load();
- let hasChanges = false;
- for (let key in newData) {
- if (newData[key] !== settings11[key]) {
- hasChanges = true;
- settings11[key] = newData[key];
- }
- }
- if (hasChanges)
- triggerListeners();
- }, 500));
- const control = {
- _assign(obj) {
- Object.assign(settings11, obj);
- save();
- triggerListeners();
- },
- _reset() {
- control._assign(defaults);
- },
- _subscribe(callback) {
- listeners.add(callback);
- return () => listeners.delete(callback);
- },
- _unsubscribe(callback) {
- listeners.delete(callback);
- },
- get _defaults() {
- return defaults;
- }
- };
- return new Proxy(settings11, {
- get(_, prop) {
- if (isOfType(prop, prop in control))
- return control[prop];
- if (isOfType(prop, prop in settings11))
- return settings11[prop];
- throw new Error(`SyncedStorage: property "${String(prop)}" does not exist in "${localStorageKey}".`);
- },
- set(_, prop, value) {
- if (isOfType(prop, prop in settings11)) {
- settings11[prop] = value;
- save();
- triggerListeners();
- return true;
- }
- throw new Error(`Trying to set an unknown "${localStorageKey}" property "${String(prop)}"`);
- }
- });
- }
-
- // src/serializers.ts
- const SERIALIZERS = [
- {
- urlMatches: /^boards\.4chan(nel)?.org/i,
- threadSerializer: {
- selector: ".board .thread",
- serializer: fortunePostSerializer
- },
- catalogSerializer: {
- selector: "#threads",
- serializer: (thread) => thread.querySelector("a")?.href
- }
- },
- {
- urlMatches: /^thebarchive\.com/i,
- threadSerializer: {
- selector: ".thread .posts",
- serializer: theBArchivePostSerializer
- },
- catalogSerializer: {
- selector: "#thread_o_matic",
- serializer: (thread) => thread.querySelector("a.thread_image_link")?.href
- }
- }
- ];
- function fortunePostSerializer(post) {
- const titleAnchor = post.querySelector(".fileText a");
- const url = post.querySelector("a.fileThumb")?.href;
- const thumbnailUrl = post.querySelector("a.fileThumb img")?.src;
- const meta = post.querySelector(".fileText")?.textContent?.match(/\(([^\(\)]+ *, *\d+x\d+)\)/)?.[1];
- const [size, dimensions] = meta?.split(",").map((str) => str.trim()) || [];
- const [width, height] = dimensions?.split("x").map((str) => parseInt(str, 10) || void 0) || [];
- const filename = titleAnchor?.title || titleAnchor?.textContent || url?.match(/\/([^\/]+)$/)?.[1];
- if (!url || !thumbnailUrl || !filename)
- return null;
- return {
- media: [{url, thumbnailUrl, filename, size, width, height}],
- replies: post.querySelectorAll(".postInfo .backlink a.quotelink")?.length ?? 0
- };
- }
- function theBArchivePostSerializer(post) {
- const titleElement = post.querySelector(".post_file_filename");
- const url = post.querySelector("a.thread_image_link")?.href;
- const thumbnailUrl = post.querySelector("img.post_image")?.src;
- const meta = post.querySelector(".post_file_metadata")?.textContent;
- const [size, dimensions] = meta?.split(",").map((str) => str.trim()) || [];
- const [width, height] = dimensions?.split("x").map((str) => parseInt(str, 10) || void 0) || [];
- const filename = titleElement?.title || titleElement?.textContent || url?.match(/\/([^\/]+)$/)?.[1];
- if (!url || !thumbnailUrl || !filename)
- return null;
- return {
- media: [{url, size, width, height, thumbnailUrl, filename}],
- replies: post.querySelectorAll(".backlink_list a.backlink")?.length ?? 0
- };
- }
-
- // src/lib/preact.ts
- const h = preact.h;
- const render = preact.render;
- const createContext = preact.createContext;
- const useState = preactHooks.useState;
- const useEffect = preactHooks.useEffect;
- const useLayoutEffect = preactHooks.useLayoutEffect;
- const useRef = preactHooks.useRef;
- const useMemo = preactHooks.useMemo;
- const useCallback = preactHooks.useCallback;
- const useContext = preactHooks.useContext;
-
- // src/settings.ts
- const defaultSettings = {
- lastAcknowledgedVersion: "2.2.0",
- mediaListWidth: 640,
- mediaListHeight: 0.5,
- mediaListItemsPerRow: 3,
- thumbnailFit: "contain",
- volume: 0.5,
- fastForwardActivation: "hold",
- fastForwardRate: 5,
- adjustVolumeBy: 0.125,
- adjustSpeedBy: 0.5,
- seekBy: 5,
- tinySeekBy: 0.033,
- endTimeFormat: "total",
- fpmActivation: "hold",
- fpmVideoUpscaleThreshold: 0.5,
- fpmVideoUpscaleLimit: 2,
- fpmImageUpscaleThreshold: 0,
- fpmImageUpscaleLimit: 2,
- catalogNavigator: true,
- keyToggleUI: "`",
- keyNavLeft: "a",
- keyNavRight: "d",
- keyNavUp: "w",
- keyNavDown: "s",
- keyNavPageBack: "PageUp",
- keyNavPageForward: "PageDown",
- keyNavStart: "Home",
- keyNavEnd: "End",
- keyListViewToggle: "f",
- keyListViewLeft: "A",
- keyListViewRight: "D",
- keyListViewUp: "W",
- keyListViewDown: "S",
- keyViewClose: "F",
- keyViewFullPage: "Tab",
- keyViewFullScreen: "r",
- keyViewPause: "Space",
- keyViewFastForward: "Shift+Space",
- keyViewVolumeDown: "Q",
- keyViewVolumeUp: "E",
- keyViewSpeedDown: "Alt+q",
- keyViewSpeedUp: "Alt+e",
- keyViewSpeedReset: "Alt+w",
- keyViewSeekBack: "q",
- keyViewSeekForward: "e",
- keyViewTinySeekBack: "Alt+a",
- keyViewTinySeekForward: "Alt+d",
- keyViewSeekTo0: "0",
- keyViewSeekTo10: "1",
- keyViewSeekTo20: "2",
- keyViewSeekTo30: "3",
- keyViewSeekTo40: "4",
- keyViewSeekTo50: "5",
- keyViewSeekTo60: "6",
- keyViewSeekTo70: "7",
- keyViewSeekTo80: "8",
- keyViewSeekTo90: "9",
- keyCatalogOpenThread: "f",
- keyCatalogOpenThreadInNewTab: "Ctrl+F",
- keyCatalogOpenThreadInBackgroundTab: "F"
- };
- const SettingsContext = createContext(null);
- function useSettings() {
- const syncedSettings3 = useContext(SettingsContext);
- if (!syncedSettings3)
- throw new Error();
- const [_, update] = useState(NaN);
- useEffect(() => {
- return syncedSettings3._subscribe(() => update(NaN));
- }, []);
- return syncedSettings3;
- }
- const SettingsProvider = SettingsContext.Provider;
-
- // src/lib/mediaWatcher.ts
- class MediaWatcher {
- constructor(serializer2) {
- this.listeners = new Set();
- this.mediaByURL = new Map();
- this.media = [];
- this.destroy = () => {
- this.listeners.clear();
- this.observer.disconnect();
- };
- this.serialize = () => {
- let addedMedia = [];
- let hasNewMedia = false;
- let hasChanges = false;
- for (let child of this.container.children) {
- const postContainer = child;
- const serializedPost = this.serializer.serializer(postContainer);
- if (serializedPost == null)
- continue;
- for (let serializedMedia of serializedPost.media) {
- const extension = String(serializedMedia.url.match(/\.([^.]+)$/)?.[1] || "").toLowerCase();
- const mediaItem = {
- ...serializedMedia,
- extension,
- isVideo: !!extension.match(/webm|mp4/),
- isGif: extension === "gif",
- postContainer,
- replies: serializedPost.replies
- };
- let existingItem = this.mediaByURL.get(mediaItem.url);
- if (existingItem) {
- if (JSON.stringify(existingItem) !== JSON.stringify(mediaItem)) {
- Object.assign(existingItem, mediaItem);
- hasChanges = true;
- }
- continue;
- }
- this.mediaByURL.set(mediaItem.url, mediaItem);
- addedMedia.push(mediaItem);
- hasNewMedia = true;
- }
- }
- if (hasNewMedia)
- this.media = this.media.concat(addedMedia);
- if (hasNewMedia || hasChanges) {
- for (let listener of this.listeners)
- listener(addedMedia, this.media);
- }
- };
- this.subscribe = (callback) => {
- this.listeners.add(callback);
- return () => this.unsubscribe(callback);
- };
- this.unsubscribe = (callback) => {
- this.listeners.delete(callback);
- };
- this.serializer = serializer2;
- const container = document.querySelector(serializer2.selector);
- if (!container)
- throw new Error(`No elements matched by threadSelector: ${serializer2.selector}`);
- this.container = container;
- this.serialize();
- this.observer = new MutationObserver(this.serialize);
- this.observer.observe(container, {childList: true, subtree: true});
- }
- }
-
- // src/lib/catalogWatcher.ts
- class CatalogWatcher {
- constructor(serializer2) {
- this.listeners = new Set();
- this.threads = [];
- this.destroy = () => {
- this.listeners.clear();
- this.observer.disconnect();
- };
- this.serialize = () => {
- let newThreads = [];
- let hasChanges = false;
- for (let i = 0; i < this.container.children.length; i++) {
- const container = this.container.children[i];
- const url = this.serializer.serializer(container);
- if (url) {
- newThreads.push({url, container});
- if (this.threads[i]?.url !== url)
- hasChanges = true;
- }
- }
- if (hasChanges) {
- this.threads = newThreads;
- for (let listener of this.listeners)
- listener(this.threads);
- }
- };
- this.subscribe = (callback) => {
- this.listeners.add(callback);
- return () => this.unsubscribe(callback);
- };
- this.unsubscribe = (callback) => {
- this.listeners.delete(callback);
- };
- this.serializer = serializer2;
- const container = document.querySelector(serializer2.selector);
- if (!container)
- throw new Error(`No elements matched by threadSelector: ${serializer2.selector}`);
- this.container = container;
- this.serialize();
- this.observer = new MutationObserver(this.serialize);
- this.observer.observe(container, {childList: true, subtree: true});
- }
- }
-
- // src/lib/hooks.ts
- function useForceUpdate() {
- const [_, setState] = useState(NaN);
- return () => setState(NaN);
- }
- const _useKey = (() => {
- const INTERACTIVE = {INPUT: true, TEXTAREA: true, SELECT: true};
- let handlersByShortcut = {
- keydown: new Map(),
- keyup: new Map()
- };
- function triggerHandlers(event) {
- if (INTERACTIVE[event.target.nodeName])
- return;
- const eventType = event.type;
- let handlers = handlersByShortcut[eventType]?.get(keyEventId(event));
- if (handlers && handlers.length > 0) {
- event.preventDefault();
- event.stopImmediatePropagation();
- event.stopPropagation();
- handlers[handlers.length - 1](event);
- }
- }
- window.addEventListener("keydown", triggerHandlers);
- window.addEventListener("keyup", triggerHandlers);
- return function _useKey2(event, shortcut, handler) {
- useEffect(() => {
- if (!shortcut)
- return;
- let handlers = handlersByShortcut[event].get(shortcut);
- if (!handlers) {
- handlers = [];
- handlersByShortcut[event].set(shortcut, handlers);
- }
- handlers.push(handler);
- const nonNullHandlers = handlers;
- return () => {
- let indexOfHandler = nonNullHandlers.indexOf(handler);
- if (indexOfHandler >= 0)
- nonNullHandlers.splice(indexOfHandler, 1);
- };
- }, [shortcut, handler]);
- };
- })();
- function useKey(shortcut, handler) {
- _useKey("keydown", shortcut, handler);
- }
- function useKeyUp(shortcut, handler) {
- _useKey("keyup", shortcut, handler);
- }
- function useWindowDimensions() {
- let [dimensions, setDimensions] = useState([window.innerWidth, window.innerHeight]);
- useEffect(() => {
- let handleResize = throttle(() => setDimensions([window.innerWidth, window.innerHeight]), 100);
- window.addEventListener("resize", handleResize);
- return () => window.removeEventListener("resize", handleResize);
- }, []);
- return dimensions;
- }
- function useElementSize(ref, box = "border-box", throttle2 = false) {
- const [sizes, setSizes] = useState([null, null]);
- useLayoutEffect(() => {
- if (!ref.current)
- throw new Error();
- const checker = (entries) => {
- let lastEntry = entries[entries.length - 1];
- if (box === "padding-box") {
- setSizes([ref.current.clientWidth, ref.current.clientHeight]);
- } else if (box === "content-box") {
- setSizes([lastEntry.contentRect.width, lastEntry.contentRect.height]);
- } else {
- setSizes([lastEntry.borderBoxSize.inlineSize, lastEntry.borderBoxSize.blockSize]);
- }
- };
- const check = throttle2 !== false ? throttle(checker, throttle2) : checker;
- const resizeObserver = new ResizeObserver(check);
- resizeObserver.observe(ref.current);
- return () => resizeObserver.disconnect();
- }, [box]);
- return sizes;
- }
- function useItemsPerRow(ref, throttle2 = false) {
- const [itemsPerRow, setItemsPerRow] = useState(0);
- useLayoutEffect(() => {
- const container = ref.current;
- if (!container)
- throw new Error();
- const checker = () => {
- let currentTop = null;
- for (let i = 0; i < container.children.length; i++) {
- const item = container.children[i];
- const rect = item.getBoundingClientRect();
- if (currentTop != null && currentTop !== rect.top) {
- setItemsPerRow(i);
- return;
- }
- currentTop = rect.top;
- }
- setItemsPerRow(container.children.length);
- };
- const check = throttle2 !== false ? throttle(checker, throttle2) : checker;
- const resizeObserver = new ResizeObserver(check);
- resizeObserver.observe(container);
- const childrenObserver = new MutationObserver(check);
- childrenObserver.observe(container, {childList: true});
- return () => {
- resizeObserver.disconnect();
- childrenObserver.disconnect();
- };
- }, []);
- return itemsPerRow;
- }
- const useGesture = (() => {
- let callbacksByGesture = new Map();
- function startGesture({button, x, y}) {
- if (button !== 2)
- return;
- const gestureStart = {x, y};
- window.addEventListener("mouseup", endGesture);
- function endGesture({button: button2, x: x2, y: y2}) {
- window.removeEventListener("mouseup", endGesture);
- if (button2 !== 2)
- return;
- const dragDistance = Math.hypot(x2 - gestureStart.x, y2 - gestureStart.y);
- if (dragDistance < 30)
- return;
- let gesture;
- if (Math.abs(gestureStart.x - x2) < dragDistance / 2) {
- gesture = gestureStart.y < y2 ? "down" : "up";
- } else if (Math.abs(gestureStart.y - y2) < dragDistance / 2) {
- gesture = gestureStart.x < x2 ? "right" : "left";
- }
- if (gesture) {
- let callbacks = callbacksByGesture.get(gesture);
- if (callbacks && callbacks.length > 0)
- callbacks[callbacks.length - 1]();
- const preventContext = (event) => event.preventDefault();
- window.addEventListener("contextmenu", preventContext, {once: true});
- setTimeout(() => window.removeEventListener("contextmenu", preventContext), 10);
- }
- }
- }
- window.addEventListener("mousedown", startGesture);
- return function useGesture2(gesture, callback) {
- useEffect(() => {
- if (!gesture)
- return;
- let callbacks = callbacksByGesture.get(gesture);
- if (!callbacks) {
- callbacks = [];
- callbacksByGesture.set(gesture, callbacks);
- }
- callbacks.push(callback);
- const nonNullHandlers = callbacks;
- return () => {
- let callbackIndex = nonNullHandlers.indexOf(callback);
- if (callbackIndex >= 0)
- nonNullHandlers.splice(callbackIndex, 1);
- };
- }, [gesture, callback]);
- };
- })();
-
- // src/components/SideView.ts
- function SideView({onClose, children}) {
- return h("div", {class: ns("SideView")}, [h("button", {class: ns("close"), onClick: onClose}, "×"), children]);
- }
- SideView.styles = `
- /* Scrollbars in chrome since it doesn't support scrollbar-width */
- .${ns("SideView")}::-webkit-scrollbar {
- width: 10px;
- background-color: transparent;
- }
- .${ns("SideView")}::-webkit-scrollbar-track {
- border: 0;
- background-color: transparent;
- }
- .${ns("SideView")}::-webkit-scrollbar-thumb {
- border: 0;
- background-color: #6f6f70;
- }
-
- .${ns("SideView")} {
- position: fixed;
- bottom: 0;
- left: 0;
- width: var(--media-list-width);
- height: calc(100vh - var(--media-list-height));
- padding: 1em 1.5em;
- color: #aaa;
- background: #161616;
- box-shadow: 0px 6px 0 3px #0003;
- overflow-x: hidden;
- overflow-y: auto;
- scrollbar-width: thin;
- }
- .${ns("SideView")} .${ns("close")} {
- position: sticky;
- top: 0;
- float: right;
- width: 1em;
- height: 1em;
- margin: 0 -.5em 0 0;
- padding: 0;
- background: transparent;
- border: 0;
- color: #eee;
- font-size: 2em !important;
- line-height: 1;
- }
- .${ns("SideView")} > *:last-child { padding-bottom: 1em; }
- .${ns("SideView")} fieldset {
- border: 0;
- margin: 1em 0;
- padding: 0;
- }
- .${ns("SideView")} fieldset + fieldset { margin-top: 2em; }
- .${ns("SideView")} fieldset > legend {
- margin: 0
- padding: 0;
- width: 100%;
- }
- .${ns("SideView")} fieldset > legend > .${ns("title")} {
- display: inline-block;
- font-size: 1.1em;
- color: #fff;
- min-width: 38%;
- text-align: right;
- font-weight: bold;
- vertical-align: middle;
- }
- .${ns("SideView")} fieldset > legend > .${ns("actions")} { display: inline-block; margin-left: 1em; }
- .${ns("SideView")} fieldset > legend > .${ns("actions")} > button { height: 2em; margin-right: .3em; }
- .${ns("SideView")} fieldset > article {
- display: flex;
- align-items: center;
- grid-gap: .5em 1em;
- }
- .${ns("SideView")} fieldset > * + article { margin-top: .8em; }
- .${ns("SideView")} fieldset > article > header {
- flex: 0 0 38%;
- text-align: right;
- color: #fff;
- }
- .${ns("SideView")} fieldset > article > section { flex: 1 1 0; }
-
- .${ns("SideView")} fieldset.${ns("-value-heavy")} > article > header { flex: 0 0 20%; }
- .${ns("SideView")} fieldset.${ns("-compact")} > article { flex-wrap: wrap; }
- .${ns("SideView")} fieldset.${ns("-compact")} legend { text-align: left; }
- .${ns("SideView")} fieldset.${ns("-compact")} article > header {
- flex: 1 1 100%;
- margin-left: 1.5em;
- text-align: left;
- }
- .${ns("SideView")} fieldset.${ns("-compact")} article > section {
- flex: 1 1 100%;
- margin-left: 3em;
- }
- `;
-
- // src/components/Settings.ts
- const {round} = Math;
- function Settings() {
- const settings11 = useSettings();
- const containerRef = useRef(null);
- const [containerWidth] = useElementSize(containerRef, "content-box", 100);
- function handleShortcutsKeyDown(event) {
- const target = event.target;
- if (!isOfType(target, target?.nodeName === "INPUT"))
- return;
- if (target.name.indexOf("key") !== 0)
- return;
- if (event.key === "Shift")
- return;
- event.preventDefault();
- event.stopPropagation();
- settings11[target.name] = keyEventId(event);
- }
- function handleShortcutsMouseDown(event) {
- if (event.button !== 0)
- return;
- const target = event.target;
- if (!isOfType(target, target?.nodeName === "BUTTON"))
- return;
- const name = target.name;
- if (!isOfType(name, name in settings11) || name.indexOf("key") !== 0)
- return;
- if (target.value === "unbind")
- settings11[name] = null;
- else if (target.value === "reset")
- settings11[name] = defaultSettings[name];
- }
- function shortcutsFieldset(title, shortcuts) {
- function all(action) {
- settings11._assign(shortcuts.reduce((acc, [name, title2, flag]) => {
- if ((action !== "unbind" || flag !== "required") && name.indexOf("key") === 0) {
- acc[name] = action === "reset" ? defaultSettings[name] : null;
- }
- return acc;
- }, {}));
- }
- return h("fieldset", {...compactPropsS, onKeyDown: handleShortcutsKeyDown, onMouseDown: handleShortcutsMouseDown}, [
- h("legend", null, [
- h("span", {class: ns("title")}, title),
- h("span", {class: ns("actions")}, [
- h("button", {class: ns("reset"), title: "Reset category", onClick: () => all("reset")}, "↻ reset"),
- h("button", {class: ns("unbind"), title: "Unbind category", onClick: () => all("unbind")}, "⦸ unbind")
- ])
- ]),
- shortcuts.map(([name, title2, flag]) => shortcutItem(name, title2, flag))
- ]);
- }
- function shortcutItem(name, title, flag) {
- const isDefault = settings11[name] === defaultSettings[name];
- return h("article", null, [
- h("header", null, title),
- h("section", null, [
- h("input", {
- type: "text",
- name,
- value: settings11[name] || "",
- placeholder: !settings11[name] ? "unbound" : void 0
- }),
- h("button", {
- class: ns("reset"),
- name,
- value: "reset",
- title: isDefault ? "Default value" : "Reset to default",
- disabled: isDefault
- }, isDefault ? "⦿" : "↻"),
- flag === "required" ? h("button", {class: ns("unbind"), title: `Required, can't unbind`, disabled: true}, "⚠") : settings11[name] !== null && h("button", {class: ns("unbind"), name, value: "unbind", title: "Unbind"}, "⦸")
- ])
- ]);
- }
- const compactPropsM = containerWidth && containerWidth < 450 ? {class: ns("-compact")} : null;
- const compactPropsS = containerWidth && containerWidth < 340 ? compactPropsM : null;
- return h("div", {class: ns("Settings"), ref: containerRef}, [
- h("h1", null, ["Settings "]),
- h("button", {class: ns("defaults"), onClick: settings11._reset, title: "Reset all settings to default values."}, "↻ defaults"),
- h("fieldset", compactPropsM, [
- h("article", null, [
- h("header", null, "Media list width × height"),
- h("section", null, h("code", null, [
- `${settings11.mediaListWidth}px × ${round(settings11.mediaListHeight * 100)}%`,
- " ",
- h("small", null, "(drag edges)")
- ]))
- ]),
- h("article", null, [
- h("header", null, "Items per row"),
- h("section", null, [
- h("input", {
- type: "range",
- min: 2,
- max: 6,
- step: 1,
- name: "mediaListItemsPerRow",
- value: settings11.mediaListItemsPerRow,
- onInput: withValue((value) => {
- const defaultValue = settings11._defaults.mediaListItemsPerRow;
- settings11.mediaListItemsPerRow = parseInt(value, 10) || defaultValue;
- })
- }),
- " ",
- h("code", null, settings11.mediaListItemsPerRow)
- ])
- ]),
- h("article", null, [
- h("header", null, "Thumbnail fit"),
- h("section", null, [
- h("label", null, [
- h("input", {
- type: "radio",
- name: "thumbnailFit",
- value: "contain",
- checked: settings11.thumbnailFit === "contain",
- onInput: () => settings11.thumbnailFit = "contain"
- }),
- " contain"
- ]),
- h("label", null, [
- h("input", {
- type: "radio",
- name: "thumbnailFit",
- value: "cover",
- checked: settings11.thumbnailFit === "cover",
- onInput: () => settings11.thumbnailFit = "cover"
- }),
- " cover"
- ])
- ])
- ])
- ]),
- h("fieldset", compactPropsM, [
- h("legend", null, h("span", {class: ns("title")}, "Full page mode")),
- h("article", null, [
- h("header", null, [
- "Activation ",
- h("small", {class: ns("-muted")}, [
- "key: ",
- h("kbd", {title: "Rebind below."}, `${settings11.keyViewFullPage}`)
- ])
- ]),
- h("section", null, [
- h("label", null, [
- h("input", {
- type: "radio",
- name: "fpmActivation",
- value: "hold",
- checked: settings11.fpmActivation === "hold",
- onInput: () => settings11.fpmActivation = "hold"
- }),
- " hold"
- ]),
- h("label", null, [
- h("input", {
- type: "radio",
- name: "fpmActivation",
- value: "toggle",
- checked: settings11.fpmActivation === "toggle",
- onInput: () => settings11.fpmActivation = "toggle"
- }),
- " toggle"
- ])
- ])
- ]),
- h("article", null, [
- h("header", {
- title: `Upscale only videos that cover less than ${round(round(settings11.fpmVideoUpscaleThreshold * 100))}% of the available dimensions (width<threshold and height<threshold).
- Set to 100% to always upscale if video is smaller than available area.
- Set to 0% to never upscale.`
- }, [h("span", {class: ns("help-indicator")}), " Video upscale threshold"]),
- h("section", null, [
- h("input", {
- type: "range",
- min: 0,
- max: 1,
- step: 0.05,
- name: "fpmVideoUpscaleThreshold",
- value: settings11.fpmVideoUpscaleThreshold,
- onInput: withValue((value) => settings11.fpmVideoUpscaleThreshold = parseFloat(value) || 0)
- }),
- " ",
- h("code", null, settings11.fpmVideoUpscaleThreshold === 0 ? "⦸" : `${round(settings11.fpmVideoUpscaleThreshold * 100)}%`)
- ])
- ]),
- h("article", null, [
- h("header", {
- title: `Don't upscale videos more than ${settings11.fpmVideoUpscaleLimit}x of their original size.`
- }, [h("span", {class: ns("help-indicator")}), " Video upscale limit"]),
- h("section", null, [
- h("input", {
- type: "range",
- min: 1,
- max: 10,
- step: 0.5,
- name: "fpmVideoUpscaleLimit",
- value: settings11.fpmVideoUpscaleLimit,
- onInput: withValue((value) => settings11.fpmVideoUpscaleLimit = parseInt(value, 10) || 0.025)
- }),
- " ",
- h("code", null, settings11.fpmVideoUpscaleLimit === 1 ? "⦸" : `${settings11.fpmVideoUpscaleLimit}x`)
- ])
- ]),
- h("article", null, [
- h("header", {
- class: ns("title"),
- title: `Upscale only images that cover less than ${round(round(settings11.fpmImageUpscaleThreshold * 100))}% of the available dimensions (width<threshold and height<threshold).
- Set to 100% to always upscale if image is smaller than available area.
- Set to 0% to never upscale.`
- }, [h("span", {class: ns("help-indicator")}), " Image upscale threshold"]),
- h("section", null, [
- h("input", {
- type: "range",
- min: 0,
- max: 1,
- step: 0.05,
- name: "fpmImageUpscaleThreshold",
- value: settings11.fpmImageUpscaleThreshold,
- onInput: withValue((value) => settings11.fpmImageUpscaleThreshold = parseFloat(value) || 0)
- }),
- " ",
- h("code", null, settings11.fpmImageUpscaleThreshold === 0 ? "⦸" : `${round(settings11.fpmImageUpscaleThreshold * 100)}%`)
- ])
- ]),
- h("article", null, [
- h("header", {
- title: `Don't upscale images more than ${settings11.fpmImageUpscaleLimit}x of their original size.`
- }, [h("span", {class: ns("help-indicator")}), " Image upscale limit"]),
- h("section", null, [
- h("input", {
- type: "range",
- min: 1,
- max: 10,
- step: 0.5,
- name: "fpmImageUpscaleLimit",
- value: settings11.fpmImageUpscaleLimit,
- onInput: withValue((value) => settings11.fpmImageUpscaleLimit = parseInt(value, 10) || 0.025)
- }),
- " ",
- h("code", null, settings11.fpmImageUpscaleLimit === 1 ? "⦸" : `${settings11.fpmImageUpscaleLimit}x`)
- ])
- ])
- ]),
- h("fieldset", compactPropsM, [
- h("legend", null, h("span", {class: ns("title")}, "Video player")),
- h("article", null, [
- h("header", null, "Volume"),
- h("section", null, [
- h("input", {
- type: "range",
- min: 0,
- max: 1,
- step: settings11.adjustVolumeBy,
- name: "volume",
- value: settings11.volume,
- onInput: withValue((value) => settings11.volume = parseFloat(value) || 0.025)
- }),
- " ",
- h("code", null, `${(settings11.volume * 100).toFixed(1)}%`)
- ])
- ]),
- h("article", null, [
- h("header", null, "Adjust volume by"),
- h("section", null, [
- h("input", {
- type: "range",
- min: 0.025,
- max: 0.5,
- step: 0.025,
- name: "adjustVolumeBy",
- value: settings11.adjustVolumeBy,
- onInput: withValue((value) => settings11.adjustVolumeBy = parseFloat(value) || 0.025)
- }),
- " ",
- h("code", null, `${(settings11.adjustVolumeBy * 100).toFixed(1)}%`)
- ])
- ]),
- h("article", null, [
- h("header", null, "Adjust speed by"),
- h("section", null, [
- h("input", {
- type: "range",
- min: 0.05,
- max: 1,
- step: 0.05,
- name: "adjustSpeedBy",
- value: settings11.adjustSpeedBy,
- onInput: withValue((value) => settings11.adjustSpeedBy = parseFloat(value) || 0.025)
- }),
- " ",
- h("code", null, `${(settings11.adjustSpeedBy * 100).toFixed(1)}%`)
- ])
- ]),
- h("article", null, [
- h("header", null, "Seek by"),
- h("section", null, [
- h("input", {
- type: "range",
- min: 1,
- max: 60,
- step: 1,
- name: "seekBy",
- value: settings11.seekBy,
- onInput: withValue((value) => settings11.seekBy = parseInt(value, 10) || 0.025)
- }),
- " ",
- h("code", null, `${settings11.seekBy} seconds`)
- ])
- ]),
- h("article", null, [
- h("header", null, "Tiny seek by"),
- h("section", null, [
- h("input", {
- type: "range",
- min: 1e-3,
- max: 1,
- step: 1e-3,
- name: "tinySeekBy",
- value: settings11.tinySeekBy,
- onInput: withValue((value) => settings11.tinySeekBy = parseFloat(value) || 0.03)
- }),
- " ",
- h("code", null, `${round(settings11.tinySeekBy * 1e3)} ms`)
- ])
- ]),
- h("article", null, [
- h("header", null, "End time format"),
- h("section", null, [
- h("label", null, [
- h("input", {
- type: "radio",
- name: "endTimeFormat",
- value: "total",
- checked: settings11.endTimeFormat === "total",
- onInput: () => settings11.endTimeFormat = "total"
- }),
- " total"
- ]),
- h("label", null, [
- h("input", {
- type: "radio",
- name: "endTimeFormat",
- value: "remaining",
- checked: settings11.endTimeFormat === "remaining",
- onInput: () => settings11.endTimeFormat = "remaining"
- }),
- " remaining"
- ])
- ])
- ]),
- h("article", null, [
- h("header", null, [
- "Fast forward activation",
- " ",
- h("small", {class: ns("-muted"), style: "white-space: nowrap"}, [
- "key: ",
- h("kbd", {title: "Rebind below."}, `${settings11.keyViewFastForward}`)
- ])
- ]),
- h("section", null, [
- h("label", null, [
- h("input", {
- type: "radio",
- name: "fastForwardActivation",
- value: "hold",
- checked: settings11.fastForwardActivation === "hold",
- onInput: () => settings11.fastForwardActivation = "hold"
- }),
- " hold"
- ]),
- h("label", null, [
- h("input", {
- type: "radio",
- name: "fastForwardActivation",
- value: "toggle",
- checked: settings11.fastForwardActivation === "toggle",
- onInput: () => settings11.fastForwardActivation = "toggle"
- }),
- " toggle"
- ])
- ])
- ]),
- h("article", null, [
- h("header", null, "Fast forward rate"),
- h("section", null, [
- h("input", {
- type: "range",
- min: 1.5,
- max: 10,
- step: 0.5,
- name: "fastForwardRate",
- value: settings11.fastForwardRate,
- onInput: withValue((value) => settings11.fastForwardRate = Math.max(1, parseFloat(value) || 2))
- }),
- " ",
- h("code", null, `${settings11.fastForwardRate.toFixed(1)}x`)
- ])
- ])
- ]),
- h("fieldset", null, [
- h("legend", null, h("span", {class: ns("title")}, "Catalog navigator")),
- h("article", null, [
- h("header", null, "Enabled"),
- h("section", null, [
- h("input", {
- type: "checkbox",
- name: "catalogNavigator",
- value: "toggle",
- checked: settings11.catalogNavigator,
- onInput: (event) => settings11.catalogNavigator = event.target?.checked
- })
- ])
- ])
- ]),
- shortcutsFieldset("Navigation shortcuts", [
- ["keyToggleUI", "Toggle UI", "required"],
- ["keyNavLeft", "Select left"],
- ["keyNavRight", "Select right"],
- ["keyNavUp", "Select up"],
- ["keyNavDown", "Select down"],
- ["keyNavPageBack", "Page back"],
- ["keyNavPageForward", "Page forward"],
- ["keyNavStart", "To start"],
- ["keyNavEnd", "To end"]
- ]),
- shortcutsFieldset("Media list shortcuts", [
- ["keyListViewToggle", "View selected"],
- ["keyListViewLeft", "Select left & view"],
- ["keyListViewRight", "Select right & view"],
- ["keyListViewUp", "Select up & view"],
- ["keyListViewDown", "Select down & view"]
- ]),
- shortcutsFieldset("Media view shortcuts", [
- ["keyViewClose", "Close view"],
- ["keyViewFullPage", "Full page mode"],
- ["keyViewFullScreen", "Full screen mode"],
- ["keyViewPause", "Pause"],
- ["keyViewFastForward", "Fast forward"],
- ["keyViewVolumeDown", "Volume down"],
- ["keyViewVolumeUp", "Volume up"],
- ["keyViewSpeedDown", "Speed down"],
- ["keyViewSpeedUp", "Speed up"],
- ["keyViewSpeedReset", "Speed reset"],
- ["keyViewSeekBack", "Seek back"],
- ["keyViewSeekForward", "Seek forward"],
- ["keyViewTinySeekBack", "Tiny seek back"],
- ["keyViewTinySeekForward", "Tiny seek forward"],
- ["keyViewSeekTo0", "Seek to 0%"],
- ["keyViewSeekTo10", "Seek to 10%"],
- ["keyViewSeekTo20", "Seek to 20%"],
- ["keyViewSeekTo30", "Seek to 30%"],
- ["keyViewSeekTo40", "Seek to 40%"],
- ["keyViewSeekTo50", "Seek to 50%"],
- ["keyViewSeekTo60", "Seek to 60%"],
- ["keyViewSeekTo70", "Seek to 70%"],
- ["keyViewSeekTo80", "Seek to 80%"],
- ["keyViewSeekTo90", "Seek to 90%"]
- ]),
- shortcutsFieldset("Catalog shortcuts", [
- ["keyCatalogOpenThread", "Open"],
- ["keyCatalogOpenThreadInNewTab", "Open in new tab"],
- ["keyCatalogOpenThreadInBackgroundTab", "Open in background tab"]
- ])
- ]);
- }
- Settings.styles = `
- .${ns("Settings")} .${ns("defaults")} {
- position: absolute;
- top: 1em; right: 4em;
- height: 2em;
- }
- .${ns("Settings")} label {
- margin-right: .5em;
- background: #fff1;
- padding: .3em;
- border-radius: 2px;
- }
- .${ns("Settings")} input::placeholder {
- font-style: italic;
- color: #000a;
- font-size: .9em;
- }
- .${ns("Settings")} button.${ns("reset")}:not(:disabled):hover {
- color: #fff;
- border-color: #1196bf;
- background: #1196bf;
- }
- .${ns("Settings")} button.${ns("unbind")}:not(:disabled):hover {
- color: #fff;
- border-color: #f44;
- background: #f44;
- }
- .${ns("Settings")} article button.${ns("reset")},
- .${ns("Settings")} article button.${ns("unbind")} { margin-left: 0.3em; }
- `;
-
- // src/components/Help.ts
- function Help() {
- const s = useSettings();
- return h("div", {class: ns("Help")}, [
- h("h1", null, "Help"),
- h("fieldset", {class: ns("-value-heavy")}, [
- h("article", null, [
- h("header", null, "Registry"),
- h("section", null, h("a", {href: "https://greasyfork.org/en/scripts/408038-thread-media-viewer"}, "greasyfork.org/en/scripts/408038"))
- ]),
- h("article", null, [
- h("header", null, "Repository"),
- h("section", null, h("a", {href: "https://github.com/qimasho/thread-media-viewer"}, "github.com/qimasho/thread-media-viewer"))
- ]),
- h("article", null, [
- h("header", null, "Issues"),
- h("section", null, h("a", {href: "https://github.com/qimasho/thread-media-viewer/issues"}, "github.com/qimasho/thread-media-viewer/issues"))
- ])
- ]),
- h("h2", null, "Mouse controls"),
- h("ul", {class: ns("-clean")}, [
- h("li", null, ["Right button gesture ", h("kbd", null, "↑"), " to toggle media list."]),
- h("li", null, ["Right button gesture ", h("kbd", null, "↓"), " to close media view."]),
- h("li", null, [h("kbd", null, "click"), " on thumbnail (thread or list) to open media viewer."]),
- h("li", null, [
- h("kbd", null, "click"),
- " on text portion of thumbnail (thread media list) or thread title/snippet (catalog) to move cursor to that item."
- ]),
- h("li", null, [h("kbd", null, "shift+click"), " on thumbnail (thread) to open both media view and list."]),
- h("li", null, [h("kbd", null, "double-click"), " to toggle fullscreen."]),
- h("li", null, [h("kbd", null, "mouse wheel"), " on video to change audio volume."]),
- h("li", null, [h("kbd", null, "mouse wheel"), " on timeline to seek video."]),
- h("li", null, [h("kbd", null, "mouse down"), " on image for 1:1 zoom and pan."])
- ]),
- h("h2", null, "FAQ"),
- h("dl", null, [
- h("dt", null, "Why does the page scroll when I'm navigating items?"),
- h("dd", null, "It scrolls to place the associated post right below the media list box."),
- h("dt", null, "What are the small squares at the bottom of thumbnails?"),
- h("dd", null, "Visualization of the number of replies the post has.")
- ])
- ]);
- }
-
- // src/components/Changelog.ts
- const TITLE = (version, date) => h("h2", null, h("code", null, [version, h("span", {class: ns("-muted")}, " ⬩ "), h("small", null, date)]));
- function Changelog() {
- const settings11 = useSettings();
- if (settings11.lastAcknowledgedVersion !== defaultSettings.lastAcknowledgedVersion) {
- settings11.lastAcknowledgedVersion = defaultSettings.lastAcknowledgedVersion;
- }
- return h("div", {class: ns("Changelog")}, [
- h("h1", null, "Changelog"),
- TITLE("2.2.0", "2020.09.15"),
- h("ul", null, [
- h("li", null, "Added shortcuts to adjust video speed and setting for the adjustment amount."),
- h("li", null, `Added shortcuts for tiny video seeking by configurable amount of milliseconds. This is a poor man's frame step in an environment where we don't know video framerate.`)
- ]),
- TITLE("2.1.2", "2020.09.14"),
- h("ul", null, [
- h("li", null, "Style tweaks."),
- h("li", null, "Added an option to click on the text portion of the thumbnail (media list) or thread title/snippet (catalog) to move cursor to that item.")
- ]),
- TITLE("2.1.1", "2020.09.13"),
- h("ul", null, [
- h("li", null, 'Added "Thumbnail fit" setting.'),
- h("li", null, "Catalog cursor now pre-selects the item that is closest to the center of the screen instead of always the 1st one."),
- h("li", null, "Added new version indicator (changelog button turns green until clicked)"),
- h("li", null, "Fixed video pausing when clicked with other then primary mouse buttons.")
- ]),
- TITLE("2.0.0", "2020.09.12"),
- h("ul", null, [
- h("li", null, "Complete rewrite in TypeScript and restructure into a proper code base (", h("a", {href: "https://github.com/qimasho/thread-media-viewer"}, "github"), ")."),
- h("li", null, "Added catalog navigation to use same shortcuts to browse and open threads in catalogs."),
- h("li", null, "Added settings with knobs for pretty much everything."),
- h("li", null, "Added changelog (hi)."),
- h("li", null, `Further optimized all media viewing features and interactions so they are more robust, stable, and responsive (except enter/exit fullscreen, all glitchiness and slow transitions there are browser's fault and I can't do anything about it T.T).`)
- ])
- ]);
- }
-
- // src/components/SideNav.ts
- function SideNav({active, onActive}) {
- const settings11 = useSettings();
- const isNewVersion = settings11.lastAcknowledgedVersion !== defaultSettings.lastAcknowledgedVersion;
- function button(name, title, className) {
- let classNames = "";
- if (active === name)
- classNames += ` ${ns("-active")}`;
- if (className)
- classNames += ` ${className}`;
- return h("button", {class: classNames, onClick: () => onActive(name)}, title);
- }
- return h("div", {class: ns("SideNav")}, [
- button("settings", "⚙ settings"),
- button("help", "? help"),
- button("changelog", "☲ changelog", isNewVersion ? ns("-success") : void 0)
- ]);
- }
- SideNav.styles = `
- .${ns("SideNav")} { display: flex; min-width: 0; }
- .${ns("SideNav")} > button,
- .${ns("SideNav")} > button:active {
- color: #eee;
- background: #1c1c1c;
- border: 0;
- outline: 0;
- border-radius: 2px;
- font-size: .911em;
- line-height: 1;
- height: 20px;
- padding: 0 .5em;
- white-space: nowrap;
- overflow: hidden;
- }
- .${ns("SideNav")} > button:hover {
- color: #fff;
- background: #333;
- }
- .${ns("SideNav")} > button + button {
- margin-left: 2px;
- }
- .${ns("SideNav")} > button.${ns("-active")} {
- color: #222;
- background: #ccc;
- }
- .${ns("SideNav")} > button.${ns("-success")} {
- color: #fff;
- background: #4b663f;
- }
- .${ns("SideNav")} > button.${ns("-success")}:hover {
- background: #b6eaa0;
- }
- `;
-
- // src/components/MediaList.ts
- const {max, min, round: round2} = Math;
- function MediaList({
- media,
- activeIndex,
- sideView,
- onActivation,
- onOpenSideView
- }) {
- const settings11 = useSettings();
- const containerRef = useRef(null);
- const listRef = useRef(null);
- let [selectedIndex, setSelectedIndex] = useState(activeIndex);
- const [isDragged, setIsDragged] = useState(false);
- const itemsPerRow = settings11.mediaListItemsPerRow;
- if (selectedIndex == null) {
- const centerOffset = window.innerHeight / 2;
- let lastProximity = Infinity;
- for (let i = 0; i < media.length; i++) {
- const rect = media[i].postContainer.getBoundingClientRect();
- let proximity = Math.abs(centerOffset - rect.top);
- if (rect.top > centerOffset) {
- selectedIndex = lastProximity < proximity ? i - 1 : i;
- break;
- }
- lastProximity = proximity;
- }
- if (selectedIndex == null && media.length > 0)
- selectedIndex = media.length - 1;
- if (selectedIndex != null && selectedIndex >= 0)
- setSelectedIndex(selectedIndex);
- }
- function scrollToItem(index, behavior = "smooth") {
- const targetChild = listRef.current?.children[index];
- if (isOfType(targetChild, targetChild != null)) {
- scrollToView(targetChild, {block: "center", behavior});
- }
- }
- function selectAndScrollTo(index) {
- if (media.length > 0 && index >= 0 && index < media.length) {
- setSelectedIndex(index);
- scrollToItem(index);
- }
- }
- function initiateResize(event) {
- const target = event.target;
- const direction = target?.dataset.direction;
- if (event.detail === 2 || event.button !== 0 || !direction)
- return;
- event.preventDefault();
- event.stopPropagation();
- const initialDocumentCursor = document.documentElement.style.cursor;
- const resizeX = direction === "ew" || direction === "nwse";
- const resizeY = direction === "ns" || direction === "nwse";
- const initialCursorToRightEdgeDelta = containerRef.current ? event.clientX - containerRef.current.offsetWidth : 0;
- function handleMouseMove(event2) {
- const clampedListWidth = clamp(300, event2.clientX - initialCursorToRightEdgeDelta, window.innerWidth - 300);
- if (resizeX)
- settings11.mediaListWidth = clampedListWidth;
- const clampedListHeight = clamp(200 / window.innerHeight, event2.clientY / window.innerHeight, 1 - 200 / window.innerHeight);
- if (resizeY)
- settings11.mediaListHeight = clampedListHeight;
- }
- function handleMouseUp() {
- settings11.mediaListWidth = round2(settings11.mediaListWidth / 10) * 10;
- window.removeEventListener("mouseup", handleMouseUp);
- window.removeEventListener("mousemove", handleMouseMove);
- document.documentElement.style.cursor = initialDocumentCursor;
- setIsDragged(false);
- }
- document.documentElement.style.cursor = `${direction}-resize`;
- setIsDragged(true);
- window.addEventListener("mouseup", handleMouseUp);
- window.addEventListener("mousemove", handleMouseMove);
- }
- useEffect(() => {
- if (activeIndex != null && activeIndex != selectedIndex)
- selectAndScrollTo(activeIndex);
- }, [activeIndex]);
- useEffect(() => {
- if (selectedIndex != null)
- scrollToItem(selectedIndex, "auto");
- }, []);
- useEffect(() => {
- if (selectedIndex != null && media?.[selectedIndex]?.postContainer && containerRef.current) {
- let offset = getBoundingDocumentRect(containerRef.current).height;
- scrollToView(media[selectedIndex].postContainer, {block: round2(offset), behavior: "smooth"});
- }
- }, [selectedIndex]);
- const selectUp = () => selectedIndex != null && selectAndScrollTo(max(selectedIndex - itemsPerRow, 0));
- const selectDown = () => {
- if (selectedIndex == media.length - 1) {
- document.scrollingElement?.scrollTo({
- top: document.scrollingElement.scrollHeight,
- behavior: "smooth"
- });
- }
- if (selectedIndex != null)
- selectAndScrollTo(min(selectedIndex + itemsPerRow, media.length - 1));
- };
- const selectPrev = () => selectedIndex != null && selectAndScrollTo(max(selectedIndex - 1, 0));
- const selectNext = () => selectedIndex != null && selectAndScrollTo(min(selectedIndex + 1, media.length - 1));
- const selectPageBack = () => selectedIndex != null && selectAndScrollTo(max(selectedIndex - itemsPerRow * 3, 0));
- const selectPageForward = () => selectedIndex != null && selectAndScrollTo(min(selectedIndex + itemsPerRow * 3, media.length));
- const selectFirst = () => selectAndScrollTo(0);
- const selectLast = () => selectAndScrollTo(media.length - 1);
- const selectAndViewPrev = () => {
- if (selectedIndex != null) {
- const prevIndex = max(selectedIndex - 1, 0);
- selectAndScrollTo(prevIndex);
- onActivation(prevIndex);
- }
- };
- const selectAndViewNext = () => {
- if (selectedIndex != null) {
- const nextIndex = min(selectedIndex + 1, media.length - 1);
- selectAndScrollTo(nextIndex);
- onActivation(nextIndex);
- }
- };
- const selectAndViewUp = () => {
- if (selectedIndex != null) {
- const index = max(selectedIndex - itemsPerRow, 0);
- selectAndScrollTo(index);
- onActivation(index);
- }
- };
- const selectAndViewDown = () => {
- if (selectedIndex != null) {
- const index = min(selectedIndex + itemsPerRow, media.length - 1);
- selectAndScrollTo(index);
- onActivation(index);
- }
- };
- const toggleViewSelectedItem = () => onActivation(selectedIndex === activeIndex ? null : selectedIndex);
- useKey(settings11.keyNavLeft, selectPrev);
- useKey(settings11.keyNavRight, selectNext);
- useKey(settings11.keyNavUp, selectUp);
- useKey(settings11.keyNavDown, selectDown);
- useKey(settings11.keyListViewUp, selectAndViewUp);
- useKey(settings11.keyListViewDown, selectAndViewDown);
- useKey(settings11.keyListViewLeft, selectAndViewPrev);
- useKey(settings11.keyListViewRight, selectAndViewNext);
- useKey(settings11.keyListViewToggle, toggleViewSelectedItem);
- useKey(settings11.keyNavPageBack, selectPageBack);
- useKey(settings11.keyNavPageForward, selectPageForward);
- useKey(settings11.keyNavStart, selectFirst);
- useKey(settings11.keyNavEnd, selectLast);
- function mediaItem({url, thumbnailUrl, extension, isVideo, isGif, replies, size, width, height}, index) {
- let classNames2 = ns("item");
- if (selectedIndex === index)
- classNames2 += ` ${ns("-selected")}`;
- if (activeIndex === index)
- classNames2 += ` ${ns("-active")}`;
- function onClick(event) {
- event.preventDefault();
- setSelectedIndex(index);
- onActivation(index);
- }
- let metaStr = size;
- if (width && height) {
- const widthAndHeight = `${width}×${height}`;
- metaStr = size ? `${size}, ${widthAndHeight}` : widthAndHeight;
- }
- return h("div", {key: url, class: classNames2}, [
- h("a", {href: url, onClick}, h("img", {src: thumbnailUrl})),
- metaStr && h("span", {class: ns("meta"), onClick: () => setSelectedIndex(index)}, metaStr),
- (isVideo || isGif) && h("span", {class: ns("video-type")}, null, extension),
- replies != null && replies > 0 && h("span", {class: ns("replies")}, null, Array(replies).fill(h("span", null)))
- ]);
- }
- let classNames = ns("MediaList");
- if (settings11.thumbnailFit === "cover")
- classNames += ` ${ns("-thumbnail-fit-cover")}`;
- return h("div", {class: classNames, ref: containerRef}, [
- h("div", {class: ns("list"), ref: listRef}, media.map(mediaItem)),
- h("div", {class: ns("status-bar")}, [
- h(SideNav, {active: sideView, onActive: onOpenSideView}),
- h("div", {class: ns("position")}, [
- h("span", {class: ns("current")}, selectedIndex ? selectedIndex + 1 : 0),
- h("span", {class: ns("separator")}, "/"),
- h("span", {class: ns("total")}, media.length)
- ])
- ]),
- !isDragged && h("div", {class: ns("dragger-x"), ["data-direction"]: "ew", onMouseDown: initiateResize}),
- !isDragged && h("div", {class: ns("dragger-y"), ["data-direction"]: "ns", onMouseDown: initiateResize}),
- !isDragged && h("div", {class: ns("dragger-xy"), ["data-direction"]: "nwse", onMouseDown: initiateResize})
- ]);
- }
- MediaList.styles = `
- /* Scrollbars in chrome since it doesn't support scrollbar-width */
- .${ns("MediaList")} > .${ns("list")}::-webkit-scrollbar {
- width: 10px;
- background-color: transparent;
- }
- .${ns("MediaList")} > .${ns("list")}::-webkit-scrollbar-track {
- border: 0;
- background-color: transparent;6F6F70
- }
- .${ns("MediaList")} > .${ns("list")}::-webkit-scrollbar-thumb {
- border: 0;
- background-color: #6f6f70;
- }
-
- .${ns("MediaList")} {
- --item-border-size: 2px;
- --item-meta-height: 18px;
- --list-meta-height: 24px;
- --active-color: #fff;
- --thumbnail-fit: contain;
- position: absolute;
- top: 0;
- left: 0;
- display: grid;
- grid-template-columns: 1fr;
- grid-template-rows: 1fr var(--list-meta-height);
- width: var(--media-list-width);
- height: var(--media-list-height);
- background: #111;
- box-shadow: 0px 0px 0 3px #0003;
- }
- .${ns("MediaList")}.${ns("-thumbnail-fit-cover")} { --thumbnail-fit: cover; }
- .${ns("MediaList")} > .${ns("dragger-x")} {
- position: absolute;
- left: 100%; top: 0;
- width: 12px; height: 100%;
- cursor: ew-resize;
- z-index: 2;
- }
- .${ns("MediaList")} > .${ns("dragger-y")} {
- position: absolute;
- top: 100%; left: 0;
- width: 100%; height: 12px;
- cursor: ns-resize;
- z-index: 2;
- }
- .${ns("MediaList")} > .${ns("dragger-xy")} {
- position: absolute;
- bottom: -10px; right: -10px;
- width: 20px; height: 20px;
- cursor: nwse-resize;
- z-index: 2;
- }
- .${ns("MediaList")} > .${ns("list")} {
- display: grid;
- grid-template-columns: repeat(var(--media-list-items-per-row), 1fr);
- grid-auto-rows: var(--media-list-item-height);
- overflow-y: scroll;
- overflow-x: hidden;
- scrollbar-width: thin;
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")} {
- position: relative;
- background: none;
- border: var(--item-border-size) solid transparent;
- padding: 0;
- background-color: #222;
- background-clip: padding-box;
- outline: none;
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")}.${ns("-selected")} {
- border-color: var(--active-color);
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")}.${ns("-active")} {
- background-color: var(--active-color);
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")}.${ns("-selected")}:after {
- content: '';
- display: block;
- position: absolute;
- top: 0; left: 0;
- width: 100%;
- height: 100%;
- border: 2px solid #222a;
- pointer-events: none;
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")} img {
- display: block;
- width: 100%;
- height: calc(var(--media-list-item-height) - var(--item-meta-height) - (var(--item-border-size) * 2));
- background-clip: padding-box;
- object-fit: var(--thumbnail-fit);
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")}.${ns("-active")} img {
- border: 1px solid transparent;
- border-bottom: 0;
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")} > .${ns("meta")} {
- position: absolute;
- bottom: 0;
- left: 0;
- width: 100%;
- height: var(--item-meta-height);
- display: flex;
- align-items: center;
- justify-content: center;
- color: #fff;
- font-size: calc(var(--item-meta-height) * 0.71);
- line-height: 1;
- background: #0003;
- text-shadow: 1px 1px #0003, -1px -1px #0003, 1px -1px #0003, -1px 1px #0003,
- 0px 1px #0003, 0px -1px #0003, 1px 0px #0003, -1px 0px #0003;
- white-space: nowrap;
- overflow: hidden;
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")}.${ns("-active")} > .${ns("meta")} {
- color: #222;
- text-shadow: none;
- background: #0001;
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")} > .${ns("video-type")} {
- display: block;
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- padding: .5em .5em;
- font-size: 12px !important;
- text-transform: uppercase;
- font-weight: bold;
- line-height: 1;
- color: #222;
- background: #eeeeee88;
- border-radius: 2px;
- border: 1px solid #0000002e;
- background-clip: padding-box;
- pointer-events: none;
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")} > .${ns("replies")} {
- display: block;
- position: absolute;
- bottom: calc(var(--item-meta-height) + 2px);
- left: 0;
- width: 100%;
- display: flex;
- justify-content: center;
- flex-wrap: wrap-reverse;
- }
- .${ns("MediaList")} > .${ns("list")} > .${ns("item")} > .${ns("replies")} > span {
- display: block;
- width: 6px;
- height: 6px;
- margin: 1px;
- background: var(--active-color);
- background-clip: padding-box;
- border: 1px solid #0008;
- }
- .${ns("MediaList")} > .${ns("status-bar")} {
- display: grid;
- grid-template-columns: 1fr auto;
- grid-template-rows: 1fr;
- margin: 0 2px;
- font-size: calc(var(--list-meta-height) * 0.64);
- }
- .${ns("MediaList")} > .${ns("status-bar")} > * {
- display: flex;
- align-items: center;
- }
- .${ns("MediaList")} > .${ns("status-bar")} > .${ns("position")} {
- margin: 0 .4em;
- }
- .${ns("MediaList")} > .${ns("status-bar")} > .${ns("position")} > .${ns("current")} {
- font-weight: bold;
- }
- .${ns("MediaList")} > .${ns("status-bar")} > .${ns("position")} > .${ns("separator")} {
- font-size: 1.05em;
- margin: 0 0.15em;
- }
- `;
-
- // src/components/ErrorBox.ts
- function ErrorBox({error, message}) {
- const code = error?.code;
- const msg = error?.message || message;
- return h("div", {class: ns("ErrorBox")}, [
- code != null && h("h1", null, `Error code: ${code}`),
- h("pre", null, h("code", null, `${msg ?? "Unknown error"}`))
- ]);
- }
- ErrorBox.styles = `
- .${ns("ErrorBox")} {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 2em 2.5em;
- background: #a34;
- color: #fff;
- }
- .${ns("ErrorBox")} > h1 { font-size: 1.2em; margin: 0 0 1em; }
- .${ns("ErrorBox")} > pre { margin: 0; }
- `;
-
- // src/components/Spinner.ts
- function Spinner() {
- return h("div", {class: ns("Spinner")});
- }
- Spinner.styles = `
- .${ns("Spinner")} {
- width: 1.6em;
- height: 1.6em;
- }
- .${ns("Spinner")}::after {
- content: '';
- display: block;
- width: 100%;
- height: 100%;
- animation: Spinner-rotate 500ms infinite linear;
- border: 0.1em solid #fffa;
- border-right-color: #1d1f21aa;
- border-left-color: #1d1f21aa;
- border-radius: 50%;
- }
-
- @keyframes Spinner-rotate {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- `;
-
- // src/components/MediaImage.ts
- const {min: min2, max: max2, round: round3} = Math;
- function MediaImage({
- url,
- upscale = false,
- upscaleThreshold = 0,
- upscaleLimit = 2
- }) {
- const containerRef = useRef(null);
- const imageRef = useRef(null);
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const [zoomPan, setZoomPan] = useState(false);
- const [containerWidth, containerHeight] = useElementSize(containerRef);
- useLayoutEffect(() => {
- const image = imageRef.current;
- if (error || !image)
- return;
- let checkId = null;
- const check = () => {
- if (image.naturalWidth > 0)
- setIsLoading(false);
- else
- checkId = setTimeout(check, 50);
- };
- setError(null);
- setIsLoading(true);
- check();
- return () => checkId != null && clearTimeout(checkId);
- }, [url, error]);
- useLayoutEffect(() => {
- const image = imageRef.current;
- if (!upscale || isLoading || !image || !containerWidth || !containerHeight)
- return;
- const naturalWidth = image.naturalWidth;
- const naturalHeight = image.naturalHeight;
- if (naturalWidth < containerWidth * upscaleThreshold && naturalHeight < containerHeight * upscaleThreshold) {
- const windowAspectRatio = containerWidth / containerHeight;
- const videoAspectRatio = naturalWidth / naturalHeight;
- let newHeight, newWidth;
- if (windowAspectRatio > videoAspectRatio) {
- newHeight = min2(naturalHeight * upscaleLimit, containerHeight);
- newWidth = round3(naturalWidth * (newHeight / naturalHeight));
- } else {
- newWidth = min2(naturalWidth * upscaleLimit, containerWidth);
- newHeight = round3(naturalHeight * (newWidth / naturalWidth));
- }
- image.setAttribute("width", `${newWidth}`);
- image.setAttribute("height", `${newHeight}`);
- }
- return () => {
- image.removeAttribute("width");
- image.removeAttribute("height");
- };
- }, [isLoading, url, upscale, upscaleThreshold, upscaleLimit, containerWidth, containerHeight]);
- useLayoutEffect(() => {
- const container = containerRef.current;
- const image = imageRef.current;
- if (!zoomPan || !image || !container)
- return;
- const zoomMargin = 10;
- const previewRect = image.getBoundingClientRect();
- const zoomFactor = image.naturalWidth / previewRect.width;
- const cursorAnchorX = previewRect.left + previewRect.width / 2;
- const cursorAnchorY = previewRect.top + previewRect.height / 2;
- const availableWidth = container.clientWidth;
- const availableHeight = container.clientHeight;
- const dragWidth = max2((previewRect.width - availableWidth / zoomFactor) / 2, 0);
- const dragHeight = max2((previewRect.height - availableHeight / zoomFactor) / 2, 0);
- const translateWidth = max2((image.naturalWidth - availableWidth) / 2, 0);
- const translateHeight = max2((image.naturalHeight - availableHeight) / 2, 0);
- Object.assign(image.style, {
- maxWidth: "none",
- maxHeight: "none",
- width: "auto",
- height: "auto",
- position: "fixed",
- top: "50%",
- left: "50%"
- });
- const panTo = (x, y) => {
- const dragFactorX = dragWidth > 0 ? -((x - cursorAnchorX) / dragWidth) : 0;
- const dragFactorY = dragHeight > 0 ? -((y - cursorAnchorY) / dragHeight) : 0;
- const left = round3(min2(max2(dragFactorX * translateWidth, -translateWidth - zoomMargin), translateWidth + zoomMargin));
- const top = round3(min2(max2(dragFactorY * translateHeight, -translateHeight - zoomMargin), translateHeight + zoomMargin));
- image.style.transform = `translate(-50%, -50%) translate(${left}px, ${top}px)`;
- };
- const handleMouseMove = (event) => {
- event.preventDefault();
- event.stopPropagation();
- panTo(event.clientX, event.clientY);
- };
- const handleMouseUp = () => {
- image.style.cssText = "";
- window.removeEventListener("mouseup", handleMouseUp);
- window.removeEventListener("mousemove", handleMouseMove);
- setZoomPan(false);
- };
- panTo(zoomPan.initialX, zoomPan.initialY);
- window.addEventListener("mousemove", handleMouseMove);
- window.addEventListener("mouseup", handleMouseUp);
- }, [zoomPan]);
- function handleMouseDown(event) {
- if (event.button !== 0)
- return;
- event.preventDefault();
- setZoomPan({initialX: event.clientX, initialY: event.clientY});
- }
- if (error)
- return h(ErrorBox, {error});
- let classNames = ns("MediaImage");
- if (isLoading)
- classNames += ` ${ns("-loading")}`;
- if (zoomPan)
- classNames += ` ${ns("-zoom-pan")}`;
- return h("div", {class: classNames, ref: containerRef}, isLoading && h(Spinner, null), h("img", {
- ref: imageRef,
- onMouseDown: handleMouseDown,
- onError: () => setError(new Error("Image failed to load")),
- src: url
- }));
- }
- MediaImage.styles = `
- .${ns("MediaImage")} {
- display: flex;
- align-items: center;
- justify-content: center;
- background: #000d;
- }
- .${ns("MediaImage")}.${ns("-zoom-pan")} {
- position: fixed;
- top: 0; left: 0;
- width: 100%;
- height: 100%;
- z-index: 1000;
- }
- .${ns("MediaImage")} > .${ns("Spinner")} {
- position: absolute;
- top: 50%; left: 50%;
- transform: translate(-50%, -50%);
- font-size: 2em;
- }
- .${ns("MediaImage")} > img {
- display: block;
- max-width: 100%;
- max-height: 100vh;
- }
- .${ns("MediaImage")}.${ns("-loading")} > img {
- min-width: 200px;
- min-height: 200px;
- opacity: 0;
- }
- `;
-
- // src/components/MediaVideo.ts
- const {min: min3, max: max3, round: round4} = Math;
- function MediaVideo({
- url,
- upscale = false,
- upscaleThreshold = 0.5,
- upscaleLimit = 2
- }) {
- const settings11 = useSettings();
- const containerRef = useRef(null);
- const videoRef = useRef(null);
- const volumeRef = useRef(null);
- const [isLoading, setIsLoading] = useState(true);
- const [hasAudio, setHasAudio] = useState(false);
- const [isFastForward, setIsFastForward] = useState(false);
- const [error, setError] = useState(null);
- const [containerWidth, containerHeight] = useElementSize(containerRef);
- const [speed, setSpeed] = useState(1);
- useLayoutEffect(() => {
- const video = videoRef.current;
- if (error || !video)
- return;
- let checkId = null;
- const check = () => {
- if (video?.videoHeight > 0) {
- setHasAudio(video.audioTracks?.length > 0 || video.mozHasAudio);
- setIsLoading(false);
- } else {
- checkId = setTimeout(check, 50);
- }
- };
- setError(null);
- setIsLoading(true);
- setHasAudio(false);
- setIsFastForward(false);
- check();
- return () => checkId != null && clearTimeout(checkId);
- }, [url, error]);
- useLayoutEffect(() => {
- const container = containerRef.current;
- const video = videoRef.current;
- if (!upscale || isLoading || !video || !container || !containerWidth || !containerHeight)
- return;
- const naturalWidth = video.videoWidth;
- const naturalHeight = video.videoHeight;
- if (naturalWidth < containerWidth * upscaleThreshold && naturalHeight < containerHeight * upscaleThreshold) {
- const windowAspectRatio = containerWidth / containerHeight;
- const videoAspectRatio = naturalWidth / naturalHeight;
- let newHeight, newWidth;
- if (windowAspectRatio > videoAspectRatio) {
- newHeight = min3(naturalHeight * upscaleLimit, containerHeight);
- newWidth = round4(naturalWidth * (newHeight / naturalHeight));
- } else {
- newWidth = min3(naturalWidth * upscaleLimit, containerWidth);
- newHeight = round4(naturalHeight * (newWidth / naturalWidth));
- }
- video.style.cssText = `width:${newWidth}px;height:${newHeight}px`;
- }
- return () => {
- video.style.cssText = "";
- };
- }, [isLoading, url, upscale, upscaleThreshold, upscaleLimit, containerWidth, containerHeight]);
- function initializeVolumeDragging(event) {
- const volume = volumeRef.current;
- if (event.button !== 0 || !volume)
- return;
- event.preventDefault();
- event.stopPropagation();
- const pointerTimelineSeek = throttle((moveEvent) => {
- let {top, height} = getBoundingDocumentRect(volume);
- let pos = min3(max3(1 - (moveEvent.pageY - top) / height, 0), 1);
- settings11.volume = round4(pos / settings11.adjustVolumeBy) * settings11.adjustVolumeBy;
- }, 100);
- function unbind() {
- window.removeEventListener("mousemove", pointerTimelineSeek);
- window.removeEventListener("mouseup", unbind);
- }
- window.addEventListener("mousemove", pointerTimelineSeek);
- window.addEventListener("mouseup", unbind);
- pointerTimelineSeek(event);
- }
- function handleContainerWheel(event) {
- event.preventDefault();
- event.stopPropagation();
- settings11.volume = min3(max3(settings11.volume + settings11.adjustVolumeBy * (event.deltaY > 0 ? -1 : 1), 0), 1);
- }
- const playPause = () => {
- const video = videoRef.current;
- if (video) {
- if (video.paused || video.ended)
- video.play();
- else
- video.pause();
- }
- };
- const flashVolume = useMemo(() => {
- let timeoutId = null;
- return () => {
- const volume = volumeRef.current;
- if (timeoutId)
- clearTimeout(timeoutId);
- if (volume)
- volume.style.opacity = "1";
- timeoutId = setTimeout(() => {
- if (volume)
- volume.style.cssText = "";
- }, 400);
- };
- }, []);
- useKey(settings11.keyViewPause, playPause);
- useKey(settings11.keyViewSeekBack, () => {
- const video = videoRef.current;
- if (video)
- video.currentTime = max3(video.currentTime - settings11.seekBy, 0);
- });
- useKey(settings11.keyViewSeekForward, () => {
- const video = videoRef.current;
- if (video)
- video.currentTime = min3(video.currentTime + settings11.seekBy, video.duration);
- });
- useKey(settings11.keyViewTinySeekBack, () => {
- const video = videoRef.current;
- if (video) {
- video.pause();
- video.currentTime = max3(video.currentTime - settings11.tinySeekBy, 0);
- }
- });
- useKey(settings11.keyViewTinySeekForward, () => {
- const video = videoRef.current;
- if (video) {
- video.pause();
- video.currentTime = min3(video.currentTime + settings11.tinySeekBy, video.duration);
- }
- });
- useKey(settings11.keyViewVolumeDown, () => {
- settings11.volume = max3(settings11.volume - settings11.adjustVolumeBy, 0);
- flashVolume();
- });
- useKey(settings11.keyViewVolumeUp, () => {
- settings11.volume = min3(settings11.volume + settings11.adjustVolumeBy, 1);
- flashVolume();
- });
- useKey(settings11.keyViewSpeedDown, () => setSpeed((speed2) => Math.max(settings11.adjustSpeedBy, speed2 - settings11.adjustSpeedBy)));
- useKey(settings11.keyViewSpeedUp, () => setSpeed((speed2) => speed2 + settings11.adjustSpeedBy));
- useKey(settings11.keyViewSpeedReset, () => setSpeed(1));
- useKey(settings11.keyViewFastForward, (event) => {
- if (event.repeat)
- return;
- if (settings11.fastForwardActivation === "hold")
- setIsFastForward(true);
- else
- setIsFastForward((value) => !value);
- });
- useKeyUp(settings11.keyViewFastForward, () => {
- if (settings11.fastForwardActivation === "hold")
- setIsFastForward(false);
- });
- for (let index of [1, 2, 3, 4, 5, 6, 7, 8, 9, 0]) {
- useKey(settings11[`keyViewSeekTo${index * 10}`], () => {
- const video = videoRef.current;
- if (video) {
- if (video.duration > 0)
- video.currentTime = video.duration * (index / 10);
- }
- });
- }
- if (error)
- return h(ErrorBox, {error});
- let classNames = ns("MediaVideo");
- if (isLoading)
- classNames += ` ${ns("-loading")}`;
- return h("div", {
- class: ns("MediaVideo"),
- ref: containerRef,
- onMouseDown: ({button}) => button === 0 && playPause(),
- onWheel: handleContainerWheel
- }, [
- isLoading && h(Spinner, null),
- h("video", {
- ref: videoRef,
- autoplay: true,
- preload: false,
- controls: false,
- loop: true,
- volume: settings11.volume,
- playbackRate: isFastForward ? settings11.fastForwardRate : speed,
- onError: () => setError(new Error("Video failed to load")),
- src: url
- }),
- h(VideoTimeline, {videoRef}),
- h("div", {
- class: ns("volume"),
- ref: volumeRef,
- onMouseDown: initializeVolumeDragging,
- style: hasAudio ? "display: hidden" : ""
- }, h("div", {
- class: ns("bar"),
- style: `height: ${Number(settings11.volume) * 100}%`
- })),
- speed !== 1 && h("div", {class: ns("speed")}, `${speed.toFixed(2)}x`)
- ]);
- }
- function VideoTimeline({videoRef}) {
- const settings11 = useSettings();
- const [state, setState] = useState({progress: 0, elapsed: 0, remaining: 0, duration: 0});
- const [bufferedRanges, setBufferedRanges] = useState([]);
- const timelineRef = useRef(null);
- useEffect(() => {
- const video = videoRef.current;
- const timeline = timelineRef.current;
- if (!video || !timeline)
- return;
- const handleTimeupdate = () => {
- setState({
- progress: video.currentTime / video.duration,
- elapsed: video.currentTime,
- remaining: video.duration - video.currentTime,
- duration: video.duration
- });
- };
- const handleMouseDown = (event) => {
- if (event.button !== 0)
- return;
- event.preventDefault();
- event.stopPropagation();
- const wasPaused = video.paused;
- const pointerTimelineSeek = throttle((mouseEvent) => {
- video.pause();
- let {left, width} = getBoundingDocumentRect(timeline);
- let pos = min3(max3((mouseEvent.pageX - left) / width, 0), 1);
- video.currentTime = pos * video.duration;
- }, 100);
- const unbind = () => {
- if (!wasPaused)
- video.play();
- window.removeEventListener("mousemove", pointerTimelineSeek);
- window.removeEventListener("mouseup", unbind);
- };
- window.addEventListener("mousemove", pointerTimelineSeek);
- window.addEventListener("mouseup", unbind);
- pointerTimelineSeek(event);
- };
- const handleWheel = (event) => {
- event.preventDefault();
- event.stopPropagation();
- video.currentTime = video.currentTime + 5 * (event.deltaY > 0 ? 1 : -1);
- };
- const handleProgress = () => {
- const buffer = video.buffered;
- const duration = video.duration;
- const ranges = [];
- for (let i = 0; i < buffer.length; i++) {
- ranges.push({
- start: buffer.start(i) / duration,
- end: buffer.end(i) / duration
- });
- }
- setBufferedRanges(ranges);
- };
- const progressInterval = setInterval(() => {
- handleProgress();
- if (video.buffered.length > 0 && video.buffered.end(video.buffered.length - 1) == video.duration) {
- clearInterval(progressInterval);
- }
- }, 200);
- video.addEventListener("timeupdate", handleTimeupdate);
- timeline.addEventListener("wheel", handleWheel);
- timeline.addEventListener("mousedown", handleMouseDown);
- return () => {
- video.removeEventListener("timeupdate", handleTimeupdate);
- timeline.removeEventListener("wheel", handleWheel);
- timeline.removeEventListener("mousedown", handleMouseDown);
- };
- }, []);
- const elapsedTime = formatSeconds(state.elapsed);
- const totalTime = settings11.endTimeFormat === "total" ? formatSeconds(state.duration) : `-${formatSeconds(state.remaining)}`;
- return h("div", {class: ns("timeline"), ref: timelineRef}, [
- ...bufferedRanges.map(({start, end}) => h("div", {
- class: ns("buffered-range"),
- style: {
- left: `${start * 100}%`,
- right: `${100 - end * 100}%`
- }
- })),
- h("div", {class: ns("elapsed")}, elapsedTime),
- h("div", {class: ns("total")}, totalTime),
- h("div", {class: ns("progress"), style: `width: ${state.progress * 100}%`}, [
- h("div", {class: ns("elapsed")}, elapsedTime),
- h("div", {class: ns("total")}, totalTime)
- ])
- ]);
- }
- MediaVideo.styles = `
- .${ns("MediaVideo")} {
- --timeline-max-size: 40px;
- --timeline-min-size: 20px;
- position: relative;
- display: flex;
- max-width: 100%;
- max-height: 100vh;
- align-items: center;
- justify-content: center;
- background: #000d;
- }
- .${ns("MediaVideo")} > .${ns("Spinner")} {
- position: absolute;
- top: 50%; left: 50%;
- transform: translate(-50%, -50%);
- font-size: 2em;
- }
- .${ns("MediaVideo")} > video {
- display: block;
- max-width: 100%;
- max-height: calc(100vh - var(--timeline-min-size));
- margin: 0 auto var(--timeline-min-size);
- outline: none;
- background: #000d;
- }
- .${ns("MediaVideo")}.${ns("-loading")} > video {
- min-width: 200px;
- min-height: 200px;
- opacity: 0;
- }
- .${ns("MediaVideo")} > .${ns("timeline")} {
- position: absolute;
- left: 0; bottom: 0;
- width: 100%;
- height: var(--timeline-max-size);
- font-size: 14px !important;
- line-height: 1;
- color: #eee;
- background: #111c;
- border: 1px solid #111c;
- transition: height 100ms ease-out;
- user-select: none;
- }
- .${ns("MediaVideo")}:not(:hover) > .${ns("timeline")},
- .${ns("MediaVideo")}.${ns("zoomed")} > .${ns("timeline")} {
- height: var(--timeline-min-size);
- }
- .${ns("MediaVideo")} > .${ns("timeline")} > .${ns("buffered-range")} {
- position: absolute;
- bottom: 0;
- height: 100%;
- background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAFUlEQVQImWNgQAL/////TyqHgYEBAB5CD/FVFp/QAAAAAElFTkSuQmCC') left bottom repeat;
- opacity: .17;
- transition: right 200ms ease-out;
- }
- .${ns("MediaVideo")} > .${ns("timeline")} > .${ns("progress")} {
- height: 100%;
- background: #eee;
- clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
- }
- .${ns("MediaVideo")} > .${ns("timeline")} .${ns("elapsed")},
- .${ns("MediaVideo")} > .${ns("timeline")} .${ns("total")} {
- position: absolute;
- top: 0;
- height: 100%;
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 0 .2em;
- text-shadow: 1px 1px #000, -1px -1px #000, 1px -1px #000, -1px 1px #000, 0px 1px #000, 0px -1px #000, 1px 0px #000, -1px 0px #000;
- pointer-events: none;
- }
- .${ns("MediaVideo")} > .${ns("timeline")} .${ns("elapsed")} {left: 0;}
- .${ns("MediaVideo")} > .${ns("timeline")} .${ns("total")} {right: 0;}
- .${ns("MediaVideo")} > .${ns("timeline")} > .${ns("progress")} .${ns("elapsed")},
- .${ns("MediaVideo")} > .${ns("timeline")} > .${ns("progress")} .${ns("total")} {
- color: #111;
- text-shadow: none;
- }
-
- .${ns("MediaVideo")} > .${ns("volume")} {
- position: absolute;
- right: 10px;
- top: calc(25% - var(--timeline-min-size));
- width: 30px;
- height: 50%;
- background: #111c;
- border: 1px solid #111c;
- transition: opacity 100ms linear;
- }
- .${ns("MediaVideo")}:not(:hover) > .${ns("volume")} {opacity: 0;}
- .${ns("MediaVideo")} > .${ns("volume")} > .${ns("bar")} {
- position: absolute;
- left: 0;
- bottom: 0;
- width: 100%;
- background: #eee;
- }
- .${ns("MediaVideo")} > .${ns("speed")} {
- position: absolute;
- left: 10px;
- top: 10px;
- padding: .5em .7em;
- font-size: 0.9em;
- font-family: "Lucida Console", Monaco, monospace;
- color: #fff;
- text-shadow: 1px 1px 0 #000a, -1px -1px 0 #000a, -1px 1px 0 #000a, 1px -1px 0 #000a, 0 1px 0 #000a, 1px 0 0 #000a;
- }
- `;
-
- // src/components/MediaView.ts
- function MediaView({media: {url, isVideo}}) {
- const settings11 = useSettings();
- const containerRef = useRef(null);
- const [isExpanded, setIsExpanded] = useState(false);
- const [isFullScreen, setIsFullScreen] = useState(false);
- const toggleFullscreen = () => {
- if (containerRef.current) {
- if (!document.fullscreenElement) {
- setIsFullScreen(true);
- containerRef.current.requestFullscreen().catch((error) => {
- setIsFullScreen(false);
- });
- } else {
- setIsFullScreen(false);
- document.exitFullscreen();
- }
- }
- };
- useKey(settings11.keyViewFullScreen, toggleFullscreen);
- useKey(settings11.keyViewFullPage, (event) => {
- event.preventDefault();
- if (event.repeat)
- return;
- if (settings11.fpmActivation === "hold")
- setIsExpanded(true);
- else
- setIsExpanded((value) => !value);
- });
- useKeyUp(settings11.keyViewFullPage, () => {
- if (settings11.fpmActivation === "hold")
- setIsExpanded(false);
- });
- let classNames = ns("MediaView");
- if (isExpanded || isFullScreen)
- classNames += ` ${ns("-expanded")}`;
- return h("div", {class: classNames, ref: containerRef, onDblClick: toggleFullscreen}, isVideo ? h(MediaVideo, {
- key: url,
- url,
- upscale: isExpanded || isFullScreen,
- upscaleThreshold: settings11.fpmVideoUpscaleThreshold,
- upscaleLimit: settings11.fpmVideoUpscaleLimit
- }) : h(MediaImage, {
- key: url,
- url,
- upscale: isExpanded || isFullScreen,
- upscaleThreshold: settings11.fpmImageUpscaleThreshold,
- upscaleLimit: settings11.fpmImageUpscaleLimit
- }));
- }
- MediaView.styles = `
- .${ns("MediaView")} {
- position: absolute;
- top: 0; right: 0;
- max-width: calc(100% - var(--media-list-width));
- max-height: 100vh;
- display: flex;
- flex-direction: column;
- align-items: center;
- align-content: center;
- justify-content: center;
- }
- .${ns("MediaView")} > * {
- width: 100%;
- height: 100%;
- max-width: 100%;
- max-height: 100vh;
- }
- .${ns("MediaView")}.${ns("-expanded")} {
- max-width: 100%;
- width: 100vw;
- height: 100vh;
- z-index: 1000;
- }
- .${ns("MediaView")} > .${ns("ErrorBox")} { min-height: 200px; }
- `;
-
- // src/components/ThreadMediaViewer.ts
- const {round: round5} = Math;
- function ThreadMediaViewer({settings: settings11, watcher}) {
- const containerRef = useRef(null);
- const [isOpen, setIsOpen] = useState(false);
- const [sideView, setSideView] = useState(null);
- const [activeIndex, setActiveIndex] = useState(null);
- const [windowWidth] = useWindowDimensions();
- const forceUpdate = useForceUpdate();
- useEffect(() => {
- return watcher.subscribe(forceUpdate);
- }, [watcher]);
- useEffect(() => {
- return settings11._subscribe(forceUpdate);
- }, [settings11]);
- useEffect(() => {
- const container = containerRef.current;
- if (container) {
- const cappedListWidth = clamp(300, settings11.mediaListWidth, window.innerWidth - 300);
- container.style.setProperty("--media-list-width", `${cappedListWidth}px`);
- const itemHeight = round5((cappedListWidth - 10) / settings11.mediaListItemsPerRow);
- container.style.setProperty("--media-list-item-height", `${itemHeight}px`);
- const cappedListHeight = clamp(200 / window.innerHeight, settings11.mediaListHeight, 1 - 200 / window.innerHeight);
- container.style.setProperty("--media-list-height", `${cappedListHeight * 100}vh`);
- container.style.setProperty("--media-list-items-per-row", `${settings11.mediaListItemsPerRow}`);
- }
- }, [windowWidth, settings11.mediaListWidth, settings11.mediaListHeight, settings11.mediaListItemsPerRow]);
- useEffect(() => {
- function handleClick(event) {
- const target = event.target;
- if (!isOfType(target, !!target && "closest" in target))
- return;
- const url = target?.closest("a")?.href;
- if (url && watcher.mediaByURL.has(url)) {
- const mediaIndex = watcher.media.findIndex((media) => media.url === url);
- if (mediaIndex != null) {
- event.stopPropagation();
- event.preventDefault();
- setActiveIndex(mediaIndex);
- if (event.shiftKey)
- setIsOpen(true);
- }
- }
- }
- watcher.container.addEventListener("click", handleClick);
- return () => {
- watcher.container.removeEventListener("click", handleClick);
- };
- }, []);
- const closeSideView = () => setSideView(null);
- function toggleList() {
- setIsOpen((isOpen2) => {
- setSideView(null);
- return !isOpen2;
- });
- }
- function onOpenSideView(newView) {
- setSideView((view) => view === newView ? null : newView);
- }
- useKey(settings11.keyToggleUI, toggleList);
- useKey(settings11.keyViewClose, () => setActiveIndex(null));
- useGesture("up", toggleList);
- useGesture("down", () => setActiveIndex(null));
- let SideViewContent;
- if (sideView === "help")
- SideViewContent = Help;
- if (sideView === "settings")
- SideViewContent = Settings;
- if (sideView === "changelog")
- SideViewContent = Changelog;
- return h(SettingsProvider, {value: settings11}, h("div", {class: `${ns("ThreadMediaViewer")} ${isOpen ? ns("-is-open") : ""}`, ref: containerRef}, [
- isOpen && h(MediaList, {
- media: watcher.media,
- activeIndex,
- sideView,
- onActivation: setActiveIndex,
- onOpenSideView
- }),
- SideViewContent != null && h(SideView, {key: sideView, onClose: closeSideView}, h(SideViewContent, null)),
- activeIndex != null && watcher.media[activeIndex] && h(MediaView, {media: watcher.media[activeIndex]})
- ]));
- }
- ThreadMediaViewer.styles = `
- .${ns("ThreadMediaViewer")} {
- --media-list-width: 640px;
- --media-list-height: 50vh;
- --media-list-items-per-row: 3;
- --media-list-item-height: 160px;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 0;
- }
- `;
-
- // src/components/CatalogNavigator.ts
- const {min: min4, max: max4, sqrt, pow} = Math;
- const get2DDistance = (ax, ay, bx, by) => sqrt(pow(ax - bx, 2) + pow(ay - by, 2));
- function CatalogNavigator({settings: settings11, watcher}) {
- const catalogContainerRef = useRef(watcher.container);
- const itemsPerRow = useItemsPerRow(catalogContainerRef);
- const cursorRef = useRef(null);
- const [sideView, setSideView] = useState(null);
- const [selectedIndex, setSelectedIndex] = useState(null);
- const forceUpdate = useForceUpdate();
- const [windowWidth, windowHeight] = useWindowDimensions();
- const selectedThread = selectedIndex != null ? watcher.threads[selectedIndex] : void 0;
- const enabled = settings11.catalogNavigator;
- useEffect(() => watcher.subscribe(forceUpdate), [watcher]);
- useEffect(() => settings11._subscribe(forceUpdate), [settings11]);
- useEffect(() => {
- if (selectedThread && !enabled) {
- setSelectedIndex(null);
- return;
- }
- if (enabled && !selectedThread && watcher.threads.length > 0) {
- const centerX = window.innerWidth / 2;
- const centerY = window.innerHeight / 2;
- let closest = {distance: Infinity, index: null};
- for (let i = 0; i < watcher.threads.length; i++) {
- const rect = watcher.threads[i].container.getBoundingClientRect();
- const distance = get2DDistance(rect.left + rect.width / 2, rect.top + rect.height / 2, centerX, centerY);
- if (distance < closest.distance) {
- closest.distance = distance;
- closest.index = i;
- }
- }
- if (closest.index != null)
- setSelectedIndex(closest.index);
- }
- }, [selectedThread, watcher.threads, enabled]);
- useEffect(() => {
- const cursor = cursorRef.current;
- if (!cursor || !selectedThread || !enabled)
- return;
- const rect = getBoundingDocumentRect(selectedThread.container);
- Object.assign(cursor.style, {
- left: `${rect.left - 4}px`,
- top: `${rect.top - 4}px`,
- width: `${rect.width + 8}px`,
- height: `${rect.height + 8}px`
- });
- }, [selectedThread, watcher.threads, windowWidth, itemsPerRow, enabled]);
- useEffect(() => {
- function handleCLick(event) {
- const target = event.target;
- if (!isOfType(target, !!target && "closest" in target))
- return;
- const threadContainer = target.closest(`${watcher.serializer.selector} > *`);
- if (threadContainer) {
- const index = watcher.threads.findIndex((thread) => thread.container === threadContainer);
- if (index != null)
- setSelectedIndex(index);
- }
- }
- watcher.container.addEventListener("click", handleCLick);
- return () => watcher.container.removeEventListener("click", handleCLick);
- }, [watcher.container]);
- const navToIndex = (index) => {
- const clampedIndex = max4(0, min4(watcher.threads.length - 1, index));
- const selectedThreadContainer = watcher.threads[clampedIndex].container;
- if (selectedThreadContainer) {
- setSelectedIndex(clampedIndex);
- scrollToView(selectedThreadContainer, {block: window.innerHeight / 2 - 200, behavior: "smooth"});
- }
- };
- const navBy = (amount) => selectedIndex != null && navToIndex(selectedIndex + amount);
- const toggleSettings = () => setSideView(sideView ? null : "settings");
- useKey(settings11.keyToggleUI, toggleSettings);
- useKey(enabled && settings11.keyNavLeft, () => navBy(-1));
- useKey(enabled && settings11.keyNavRight, () => navBy(1));
- useKey(enabled && settings11.keyNavUp, () => navBy(-itemsPerRow));
- useKey(enabled && settings11.keyNavDown, () => navBy(+itemsPerRow));
- useKey(enabled && settings11.keyNavPageBack, () => navBy(-itemsPerRow * 3));
- useKey(enabled && settings11.keyNavPageForward, () => navBy(+itemsPerRow * 3));
- useKey(enabled && settings11.keyNavStart, () => navToIndex(0));
- useKey(enabled && settings11.keyNavEnd, () => navToIndex(Infinity));
- useKey(enabled && settings11.keyCatalogOpenThread, () => selectedThread && (location.href = selectedThread.url));
- useKey(enabled && settings11.keyCatalogOpenThreadInNewTab, () => {
- if (selectedThread)
- GM_openInTab(selectedThread.url, {active: true});
- });
- useKey(settings11.keyCatalogOpenThreadInBackgroundTab, () => selectedThread && GM_openInTab(selectedThread.url));
- useGesture("up", toggleSettings);
- let SideViewContent;
- if (sideView === "help")
- SideViewContent = Help;
- if (sideView === "settings")
- SideViewContent = Settings;
- if (sideView === "changelog")
- SideViewContent = Changelog;
- let classNames = ns("CatalogNavigator");
- if (sideView)
- classNames += ` ${ns("-is-open")}`;
- return h(SettingsProvider, {value: settings11}, [
- enabled && selectedThread && h("div", {class: ns("CatalogCursor"), ref: cursorRef}),
- SideViewContent && h("div", {class: classNames}, [
- h(SideView, {key: sideView, onClose: () => setSideView(null)}, h(SideViewContent, null)),
- h(SideNav, {active: sideView, onActive: setSideView})
- ])
- ]);
- }
- CatalogNavigator.styles = `
- .${ns("CatalogCursor")} {
- position: absolute;
- border: 2px dashed #fff8;
- border-radius: 2px;
- transition: all 66ms cubic-bezier(0.25, 1, 0.5, 1);
- pointer-events: none;
- }
- .${ns("CatalogCursor")}:before {
- content: '';
- display: block;
- width: 100%;
- height: 100%;
- border: 2px dashed #0006;
- border-radius: 2;
- }
- .${ns("CatalogNavigator")} {
- --media-list-width: 640px;
- --media-list-height: 50vh;
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 0;
- }
- .${ns("CatalogNavigator")} > .${ns("SideNav")} {
- position: fixed;
- left: 2px;
- bottom: calc(var(--media-list-height) - 0.2em);
- padding: 2px;
- border-radius: 3px;
- background: #161616;
- }
-
- `;
-
- // src/styles.ts
- const componentStyles = [
- ThreadMediaViewer,
- CatalogNavigator,
- ErrorBox,
- MediaImage,
- MediaList,
- MediaVideo,
- MediaView,
- Settings,
- SideNav,
- SideView,
- Spinner
- ].map(({styles: styles2}) => styles2).join("\n");
- const baseStyles = `
- .${ns("CONTAINER")},
- .${ns("CONTAINER")} *,
- .${ns("CONTAINER")} *:before,
- .${ns("CONTAINER")} *:after {
- box-sizing: border-box;
- font-family: inherit;
- line-height: 1.4;
- }
- .${ns("CONTAINER")} {
- font-family: arial, helvetica, sans-serif;
- font-size: 16px;
- color: #aaa;
- }
-
- .${ns("CONTAINER")} a { color: #c4b256 !important; }
- .${ns("CONTAINER")} a:hover { color: #fde981 !important; }
- .${ns("CONTAINER")} a:active { color: #000 !important; }
-
- .${ns("CONTAINER")} input,
- .${ns("CONTAINER")} button {
- box-sizing: border-box;
- display: inline-block;
- vertical-align: middle;
- margin: 0;
- padding: 0 0.3em;
- height: 1.6em;
- font-size: inherit;
- border-radius: 2px;
- }
- .${ns("CONTAINER")} input:focus { box-shadow: 0 0 0 3px #fff2; }
- .${ns("CONTAINER")} input[type=text] {
- border: 0 !important;
- width: 8em;
- font-family: "Lucida Console", Monaco, monospace;
- color: #222;
- }
- .${ns("CONTAINER")} input[type=text].small { width: 4em; }
- .${ns("CONTAINER")} input[type=text].large { width: 12em; }
- .${ns("CONTAINER")} input[type=range] { width: 10em; }
- .${ns("CONTAINER")} input[type=radio],
- .${ns("CONTAINER")} input[type=range],
-
- .${ns("CONTAINER")} input[type=checkbox] { padding: 0; }
- .${ns("CONTAINER")} button {
- color: #fff;
- background: transparent;
- border: 1px solid #333;
- }
- .${ns("CONTAINER")} button:not(:disabled):hover {
- color: #222;
- background: #fff;
- border-color: #fff;
- }
- .${ns("CONTAINER")} button:disabled { opacity: .5; border-color: transparent; }
-
- .${ns("CONTAINER")} h1,
- .${ns("CONTAINER")} h2,
- .${ns("CONTAINER")} h3 { margin: 0; font-weight: normal; color: #fff; }
- .${ns("CONTAINER")} * + h1,
- .${ns("CONTAINER")} * + h2,
- .${ns("CONTAINER")} * + h3 { margin-top: 1em; }
- .${ns("CONTAINER")} h1 { font-size: 1.5em !important; }
- .${ns("CONTAINER")} h2 { font-size: 1.2em !important; }
- .${ns("CONTAINER")} h3 { font-size: 1em !important; font-weight: bold; }
-
- .${ns("CONTAINER")} ul { list-style: square; padding-left: 1em; margin: 1em 0; }
- .${ns("CONTAINER")} ul.${ns("-clean")} { list-style: none; }
- .${ns("CONTAINER")} li { padding: 0.3em 0; list-style: inherit; }
- .${ns("CONTAINER")} code {
- font-family: "Lucida Console", Monaco, monospace;
- padding: 0;
- background-color: transparent;
- color: inherit;
- }
-
- .${ns("CONTAINER")} pre { white-space: pre-wrap; }
- .${ns("CONTAINER")} kbd {
- padding: .17em .2em;
- font-family: "Lucida Console", Monaco, monospace;
- color: #fff;
- font-size: .95em;
- border-radius: 2px;
- background: #363f44;
- text-shadow: -1px -1px #0006;
- border: 0;
- box-shadow: none;
- line-height: inherit;
- }
-
- .${ns("CONTAINER")} dl { margin: 1em 0; }
- .${ns("CONTAINER")} dt { font-weight: bold; }
- .${ns("CONTAINER")} dd { margin: .1em 0 .8em; color: #888; }
- .${ns("CONTAINER")} [title] { cursor: help; }
- .${ns("CONTAINER")} .${ns("help-indicator")} {
- display: inline-block;
- vertical-align: middle;
- background: #333;
- color: #aaa;
- border-radius: 50%;
- width: 1.3em;
- height: 1.3em;
- text-align: center;
- font-size: .8em;
- line-height: 1.3;
- }
-
- .${ns("CONTAINER")} .${ns("help-indicator")}::after { content: '?'; }
- .${ns("CONTAINER")} .${ns("-muted")} { opacity: .5; }
- `;
- GM_addStyle(baseStyles + componentStyles);
-
- // src/index.ts
- const serializer = SERIALIZERS.find((serializer2) => serializer2.urlMatches.exec(location.host + location.pathname));
- if (serializer) {
- const {threadSerializer, catalogSerializer} = serializer;
- const settings11 = syncedSettings(ns("settings"), defaultSettings);
- let mediaWatcher2 = null;
- let catalogWatcher2 = null;
- const container = Object.assign(document.createElement("div"), {className: ns("CONTAINER")});
- document.body.appendChild(container);
- const refreshMounts = throttle(() => {
- if (mediaWatcher2 && !document.body.contains(mediaWatcher2.container)) {
- render(null, container);
- mediaWatcher2.destroy();
- mediaWatcher2 = null;
- }
- if (catalogWatcher2 && !document.body.contains(catalogWatcher2.container)) {
- render(null, container);
- catalogWatcher2.destroy();
- catalogWatcher2 = null;
- }
- if (!mediaWatcher2 && !catalogWatcher2) {
- if (threadSerializer) {
- try {
- mediaWatcher2 = new MediaWatcher(threadSerializer);
- render(h(ThreadMediaViewer, {settings: settings11, watcher: mediaWatcher2}), container);
- } catch (error) {
- }
- }
- if (catalogSerializer) {
- try {
- catalogWatcher2 = new CatalogWatcher(catalogSerializer);
- render(h(CatalogNavigator, {settings: settings11, watcher: catalogWatcher2}), container);
- } catch (error) {
- }
- }
- }
- }, 100);
- new MutationObserver(refreshMounts).observe(document.body, {childList: true, subtree: true});
- refreshMounts();
- }
- })();