// ==UserScript==
// @name Thread Media Viewer
// @description Comfy media browser and viewer for various discussion boards.
// @version 2.4.2
// @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/[email protected]/dist/preact.min.js
// @require https://cdn.jsdelivr.net/npm/[email protected]/hooks/dist/hooks.umd.js
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addValueChangeListener
// @grant GM_removeValueChangeListener
// @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;
}
function prevented(callback) {
return function(event) {
event.preventDefault();
event.stopPropagation();
callback(event);
};
}
// src/lib/syncedSettings.ts
const localStorageDriver = {
subscribersMap: new Map(),
get: (name) => {
try {
return JSON.parse(localStorage.getItem(name));
} catch {
return void 0;
}
},
set: (name, value) => localStorage.setItem(name, JSON.stringify(value)),
subscribe: (name, callback) => {
const listener = () => callback(name, void 0, localStorage.getItem(name));
localStorageDriver.subscribersMap.set(callback, listener);
window.addEventListener("storage", listener);
return () => localStorageDriver.unsubscribe(callback);
},
unsubscribe: (callback) => {
const listener = localStorageDriver.subscribersMap.get(callback);
if (listener) {
window.removeEventListener("storage", listener);
localStorageDriver.subscribersMap.delete(callback);
}
}
};
const greaseMonkeyDriver = {
subscribersMap: new Map(),
get: (name) => GM_getValue(name),
set: (name, value) => GM_setValue(name, value),
subscribe: (name, callback) => {
const id = GM_addValueChangeListener(name, callback);
greaseMonkeyDriver.subscribersMap.set(callback, id);
return () => greaseMonkeyDriver.unsubscribe(callback);
},
unsubscribe: (callback) => {
const id = localStorageDriver.subscribersMap.get(callback);
if (id) {
GM_removeValueChangeListener(id);
greaseMonkeyDriver.subscribersMap.delete(callback);
}
}
};
function syncedSettings(storageKey, defaults, driver = greaseMonkeyDriver) {
const listeners = new Set();
let savingPromise = null;
let settings11 = load();
function triggerListeners() {
for (let callback of listeners)
callback(settings11);
}
function load() {
let data = driver.get(storageKey);
return typeof data === "object" ? {...defaults, ...data} : {...defaults};
}
function save() {
if (savingPromise)
return savingPromise;
savingPromise = new Promise((resolve) => setTimeout(() => {
driver.set(storageKey, settings11);
savingPromise = null;
resolve();
}, 10));
return savingPromise;
}
driver.subscribe(storageKey, 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 "${storageKey}".`);
},
set(_, prop, value) {
if (isOfType(prop, prop in settings11)) {
settings11[prop] = value;
save();
triggerListeners();
return true;
}
throw new Error(`Trying to set an unknown "${storageKey}" 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.4.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 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.4.0", "2020.11.05"),
h("ul", null, [
h("li", null, [
`Settings are now saved in script's greasemonkey storage instead of website's localStorage. This makes settings persist between all websites the script runs on.`,
h("br", {}),
`Your old settings should migrate automatically, in a weird case they don't - sorry for the inconvenience.`
])
]),
TITLE("2.3.0", "2020.10.26"),
h("ul", null, [
h("li", null, "Added viewer control buttons (fullpage toggle and speed).")
]),
TITLE("2.2.1", "2020.09.26"),
h("ul", null, [
h("li", null, "Fixed dragging timeline sometimes not resuming video properly.")
]),
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;
pointer-events: none;
}
.${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,
isExpanded,
onExpand
}) {
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
}), h("div", {class: ns("controls")}, [
h("button", {
onMouseDown: prevented((event) => event.button === 0 && onExpand?.()),
class: isExpanded ? ns("active") : void 0
}, "⛶"),
h("span", {class: ns("spacer")})
]));
}
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,
isExpanded,
onExpand
}) {
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);
window.addEventListener("keyup", () => setIsFastForward(false), {once: true});
} else {
setIsFastForward((value) => !value);
}
});
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("controls")}, [
h("button", {
onMouseDown: prevented((event) => event.button === 0 && onExpand?.()),
class: isExpanded ? ns("active") : void 0
}, "⛶"),
h("span", {class: ns("spacer")}),
h("button", {
onMouseDown: prevented((event) => event.button === 0 && setSpeed(1)),
class: speed === 1 ? ns("active") : void 0
}, "1x"),
h("button", {
onMouseDown: prevented((event) => event.button === 0 && setSpeed(1.5)),
class: speed === 1.5 ? ns("active") : void 0
}, "1.5x"),
h("button", {
onMouseDown: prevented((event) => event.button === 0 && setSpeed(2)),
class: speed === 2 ? ns("active") : void 0
}, "2x"),
h("span", {class: ns("spacer")}),
h("span", {class: ns("symmetry-dummy")})
]),
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 = () => {
window.removeEventListener("mousemove", pointerTimelineSeek);
window.removeEventListener("mouseup", unbind);
pointerTimelineSeek.flush();
if (!wasPaused)
video.play();
};
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("MediaView")} .${ns("MediaVideo")} > .${ns("controls")} {
bottom: var(--timeline-max-size);
}
.${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")} {
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);
window.addEventListener("keyup", () => setIsExpanded(false), {once: true});
} else {
setIsExpanded((value) => !value);
}
});
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,
isExpanded,
onExpand: () => setIsExpanded((isExpanded2) => !isExpanded2)
}) : h(MediaImage, {
key: url,
url,
upscale: isExpanded || isFullScreen,
upscaleThreshold: settings11.fpmImageUpscaleThreshold,
upscaleLimit: settings11.fpmImageUpscaleLimit,
isExpanded,
onExpand: () => setIsExpanded((isExpanded2) => !isExpanded2)
}));
}
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; }
.${ns("MediaView")} .${ns("controls")} {
display: flex;
gap: 2px;
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 0 5px 5px;
transition: all 100ms linear;
}
.${ns("MediaView")}:not(:hover) .${ns("controls")} {
opacity: 0;
}
.${ns("MediaView")} .${ns("controls")} > .${ns("spacer")} { flex: 1; opacity: 0; }
.${ns("MediaView")} .${ns("controls")} > .${ns("spacer")}::after { content: 'm'; }
.${ns("MediaView")} .${ns("controls")} > button,
.${ns("MediaView")} .${ns("controls")} > .${ns("symmetry-dummy")} {
display: block;
height: 24px;
margin: 0;
padding: 0 6px;
border: 0;
}
.${ns("MediaView")} .${ns("controls")} > button {
display: table-cell;
vertical-align: middle;
text-align: center;
flex: 0 0 auto;
color: #fff;
background: #222b;
border-radius: 2px;
font-size: 14px;
text-shadow: 1px 1px 0 #000d, -1px -1px 0 #000d, -1px 1px 0 #000d, 1px -1px 0 #000d;
}
.${ns("MediaView")} .${ns("controls")} > button:hover,
.${ns("MediaView")} .${ns("controls")} > button.${ns("active")} {
color: #111;
background: #eee;
text-shadow: none;
}
`;
// 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;
filter: none;
}
.${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;
background: #ddd;
}
.${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) {
let migratedDefaultSettings;
try {
migratedDefaultSettings = {...defaultSettings, ...JSON.parse(localStorage.getItem(ns("settings")))};
} catch {
migratedDefaultSettings = defaultSettings;
}
const {threadSerializer, catalogSerializer} = serializer;
const settings11 = syncedSettings("settings", migratedDefaultSettings);
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();
}
})();