mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-05-30 02:09:26 -07:00
website: snapshot
This commit is contained in:
353
modules/modern-screenshot/4.6.7/dist/index.d.ts
vendored
353
modules/modern-screenshot/4.6.7/dist/index.d.ts
vendored
@@ -1,353 +0,0 @@
|
||||
interface Options {
|
||||
/**
|
||||
* Width in pixels to be applied to node before rendering.
|
||||
*/
|
||||
width?: number;
|
||||
/**
|
||||
* Height in pixels to be applied to node before rendering.
|
||||
*/
|
||||
height?: number;
|
||||
/**
|
||||
* A number between `0` and `1` indicating image quality (e.g. 0.92 => 92%) of the JPEG image.
|
||||
*/
|
||||
quality?: number;
|
||||
/**
|
||||
* A string indicating the image format. The default type is image/png; that type is also used if the given type isn't supported.
|
||||
*/
|
||||
type?: string;
|
||||
/**
|
||||
* The pixel ratio of captured image.
|
||||
*
|
||||
* DPI = 96 * scale
|
||||
*
|
||||
* default: 1
|
||||
*/
|
||||
scale?: number;
|
||||
/**
|
||||
* A string value for the background color, any valid CSS color value.
|
||||
*/
|
||||
backgroundColor?: string | null;
|
||||
/**
|
||||
* An object whose properties to be copied to node's style before rendering.
|
||||
*/
|
||||
style?: Partial<CSSStyleDeclaration> | null;
|
||||
/**
|
||||
* A function taking DOM node as argument. Should return `true` if passed
|
||||
* node should be included in the output. Excluding node means excluding
|
||||
* it's children as well.
|
||||
*/
|
||||
filter?: ((el: Node) => boolean) | null;
|
||||
/**
|
||||
* Maximum canvas size (pixels).
|
||||
*
|
||||
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas#maximum_canvas_size
|
||||
*/
|
||||
maximumCanvasSize?: number;
|
||||
/**
|
||||
* Load media timeout and fetch remote asset timeout (millisecond).
|
||||
*
|
||||
* default: 30000
|
||||
*/
|
||||
timeout?: number;
|
||||
/**
|
||||
* Embed assets progress.
|
||||
*/
|
||||
progress?: ((current: number, total: number) => void) | null;
|
||||
/**
|
||||
* Enable debug mode to view the execution time log.
|
||||
*/
|
||||
debug?: boolean;
|
||||
/**
|
||||
* Custom implementation to get image data for a custom URL.
|
||||
* This can be helpful for Capacitor or Cordova when using
|
||||
* native fetch to bypass CORS issues.
|
||||
*
|
||||
* If returns a string, will completely bypass any `Options.fetch`
|
||||
* settings with your custom implementation.
|
||||
*
|
||||
* If returns false, will fall back to normal fetch implementation
|
||||
*
|
||||
* @param url
|
||||
* @returns A data URL for the image
|
||||
*/
|
||||
fetchFn?: ((url: string) => Promise<string | false>) | null;
|
||||
/**
|
||||
* The options of fetch resources.
|
||||
*/
|
||||
fetch?: {
|
||||
/**
|
||||
* The second parameter of `window.fetch` RequestInit
|
||||
*
|
||||
* default: {
|
||||
* cache: 'force-cache',
|
||||
* }
|
||||
*/
|
||||
requestInit?: RequestInit;
|
||||
/**
|
||||
* Set to `true` to append the current time as a query string to URL
|
||||
* requests to enable cache busting.
|
||||
*
|
||||
* default: false
|
||||
*/
|
||||
bypassingCache?: boolean | RegExp;
|
||||
/**
|
||||
* A data URL for a placeholder image that will be used when fetching
|
||||
* an image fails. Defaults to an empty string and will render empty
|
||||
* areas for failed images.
|
||||
*
|
||||
* default: data:image/png;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
|
||||
*/
|
||||
placeholderImage?: string | ((cloned: HTMLImageElement | SVGImageElement) => string | Promise<string>);
|
||||
};
|
||||
/**
|
||||
* The options of fonts download and embed.
|
||||
*/
|
||||
font?: false | {
|
||||
/**
|
||||
* Font minify
|
||||
*/
|
||||
minify?: (font: ArrayBuffer, subset: string) => ArrayBuffer;
|
||||
/**
|
||||
* The preferred font format. If specified all other font formats are ignored.
|
||||
*/
|
||||
preferredFormat?: 'woff' | 'woff2' | 'truetype' | 'opentype' | 'embedded-opentype' | 'svg' | string;
|
||||
/**
|
||||
* A CSS string to specify for font embeds. If specified only this CSS will
|
||||
* be present in the resulting image.
|
||||
*/
|
||||
cssText?: string;
|
||||
};
|
||||
/**
|
||||
* All enabled features
|
||||
*
|
||||
* default: true
|
||||
*/
|
||||
features?: boolean | {
|
||||
/**
|
||||
* Copy scrollbar css styles
|
||||
*
|
||||
* default: true
|
||||
*/
|
||||
copyScrollbar?: boolean;
|
||||
/**
|
||||
* Remove abnormal attributes to cloned node (for normalize XML)
|
||||
*
|
||||
* default: true
|
||||
*/
|
||||
removeAbnormalAttributes?: boolean;
|
||||
/**
|
||||
* Remove control characters (for normalize XML)
|
||||
*
|
||||
* default: true
|
||||
*/
|
||||
removeControlCharacter?: boolean;
|
||||
/**
|
||||
* Fix svg+xml image decode (for Safari、Firefox)
|
||||
*
|
||||
* default: true
|
||||
*/
|
||||
fixSvgXmlDecode?: boolean;
|
||||
/**
|
||||
* Render scrolled children with scrolled content
|
||||
*
|
||||
* default: false
|
||||
*/
|
||||
restoreScrollPosition?: boolean;
|
||||
};
|
||||
/**
|
||||
* Canvas `drawImage` interval
|
||||
* is used to fix errors in decoding images in Safari、Firefox
|
||||
*
|
||||
* default: 100
|
||||
*/
|
||||
drawImageInterval?: number;
|
||||
/**
|
||||
* Web Worker script url
|
||||
*/
|
||||
workerUrl?: string | null;
|
||||
/**
|
||||
* Web Worker number
|
||||
*/
|
||||
workerNumber?: number;
|
||||
/**
|
||||
* Triggered after each node is cloned
|
||||
*/
|
||||
onCloneEachNode?: ((cloned: Node) => void | Promise<void>) | null;
|
||||
/**
|
||||
* Triggered after a node is cloned
|
||||
*/
|
||||
onCloneNode?: ((cloned: Node) => void | Promise<void>) | null;
|
||||
/**
|
||||
* Triggered after a node is embed
|
||||
*/
|
||||
onEmbedNode?: ((cloned: Node) => void | Promise<void>) | null;
|
||||
/**
|
||||
* Triggered after a ForeignObjectSvg is created
|
||||
*/
|
||||
onCreateForeignObjectSvg?: ((svg: SVGSVGElement) => void | Promise<void>) | null;
|
||||
/**
|
||||
* An array of style property names.
|
||||
* Can be used to manually specify which style properties are
|
||||
* included when cloning nodes.
|
||||
* This can be useful for performance-critical scenarios.
|
||||
*/
|
||||
includeStyleProperties?: string[] | null;
|
||||
}
|
||||
|
||||
interface Request {
|
||||
type: 'image' | 'text';
|
||||
resolve?: (response: string) => void;
|
||||
reject?: (error: Error) => void;
|
||||
response: Promise<string>;
|
||||
}
|
||||
interface InternalContext<T extends Node> {
|
||||
/**
|
||||
* FLAG
|
||||
*/
|
||||
__CONTEXT__: true;
|
||||
/**
|
||||
* Logger
|
||||
*/
|
||||
log: {
|
||||
time: (label: string) => void;
|
||||
timeEnd: (label: string) => void;
|
||||
warn: (...args: any[]) => void;
|
||||
};
|
||||
/**
|
||||
* Node
|
||||
*/
|
||||
node: T;
|
||||
/**
|
||||
* Owner document
|
||||
*/
|
||||
ownerDocument?: Document;
|
||||
/**
|
||||
* Owner window
|
||||
*/
|
||||
ownerWindow?: Window;
|
||||
/**
|
||||
* DPI
|
||||
*
|
||||
* scale === 1 ? null : 96 * scale
|
||||
*/
|
||||
dpi: number | null;
|
||||
/**
|
||||
* The `style` element under the root `svg` element
|
||||
*/
|
||||
svgStyleElement?: HTMLStyleElement;
|
||||
/**
|
||||
* The `defs` element under the root `svg` element
|
||||
*/
|
||||
svgDefsElement?: SVGDefsElement;
|
||||
/**
|
||||
* The `svgStyleElement` class styles
|
||||
*
|
||||
* Map<cssText, class[]>
|
||||
*/
|
||||
svgStyles: Map<string, string[]>;
|
||||
/**
|
||||
* The map of default `getComputedStyle` for all tagnames
|
||||
*/
|
||||
defaultComputedStyles: Map<string, Map<string, any>>;
|
||||
/**
|
||||
* The IFrame sandbox used to get the `defaultComputedStyles`
|
||||
*/
|
||||
sandbox?: HTMLIFrameElement;
|
||||
/**
|
||||
* Web Workers
|
||||
*/
|
||||
workers: Worker[];
|
||||
/**
|
||||
* The map of `font-family` values for all cloend elements
|
||||
*/
|
||||
fontFamilies: Map<string, Set<string>>;
|
||||
/**
|
||||
* Map<CssUrl, DataUrl>
|
||||
*/
|
||||
fontCssTexts: Map<string, string>;
|
||||
/**
|
||||
* `headers.accept` to use when `window.fetch` fetches images
|
||||
*/
|
||||
acceptOfImage: string;
|
||||
/**
|
||||
* All requests for `fetch`
|
||||
*/
|
||||
requests: Map<string, Request>;
|
||||
/**
|
||||
* Canvas multiple draw image fix svg+xml image decoding in Safari and Firefox
|
||||
*/
|
||||
drawImageCount: number;
|
||||
/**
|
||||
* Wait for all tasks embedded in
|
||||
*/
|
||||
tasks: Promise<void>[];
|
||||
/**
|
||||
* Automatically destroy context
|
||||
*/
|
||||
autoDestruct: boolean;
|
||||
/**
|
||||
* Is enable
|
||||
*
|
||||
* @param key
|
||||
*/
|
||||
isEnable: (key: string) => boolean;
|
||||
/**
|
||||
* [cloning phase] To get the node style set by the user
|
||||
*/
|
||||
currentNodeStyle?: Map<string, [string, string]>;
|
||||
currentParentNodeStyle?: Map<string, [string, string]>;
|
||||
/**
|
||||
* [cloning phase] shadowDOM root list
|
||||
*/
|
||||
shadowRoots: ShadowRoot[];
|
||||
}
|
||||
type Context<T extends Node = Node> = InternalContext<T> & Required<Options>;
|
||||
|
||||
declare function domToBlob<T extends Node>(node: T, options?: Options): Promise<Blob>;
|
||||
declare function domToBlob<T extends Node>(context: Context<T>): Promise<Blob>;
|
||||
|
||||
declare function domToCanvas<T extends Node>(node: T, options?: Options): Promise<HTMLCanvasElement>;
|
||||
declare function domToCanvas<T extends Node>(context: Context<T>): Promise<HTMLCanvasElement>;
|
||||
|
||||
declare function domToDataUrl<T extends Node>(node: T, options?: Options): Promise<string>;
|
||||
declare function domToDataUrl<T extends Node>(context: Context<T>): Promise<string>;
|
||||
|
||||
declare function domToForeignObjectSvg<T extends Node>(node: T, options?: Options): Promise<SVGElement>;
|
||||
declare function domToForeignObjectSvg<T extends Node>(context: Context<T>): Promise<SVGElement>;
|
||||
|
||||
declare function domToImage<T extends Node>(node: T, options?: Options): Promise<HTMLImageElement>;
|
||||
declare function domToImage<T extends Node>(context: Context<T>): Promise<HTMLImageElement>;
|
||||
|
||||
declare function domToJpeg<T extends Node>(node: T, options?: Options): Promise<string>;
|
||||
declare function domToJpeg<T extends Node>(context: Context<T>): Promise<string>;
|
||||
|
||||
declare function domToPixel<T extends Node>(node: T, options?: Options): Promise<Uint8ClampedArray>;
|
||||
declare function domToPixel<T extends Node>(context: Context<T>): Promise<Uint8ClampedArray>;
|
||||
|
||||
declare function domToPng<T extends Node>(node: T, options?: Options): Promise<string>;
|
||||
declare function domToPng<T extends Node>(context: Context<T>): Promise<string>;
|
||||
|
||||
declare function domToSvg<T extends Node>(node: T, options?: Options): Promise<string>;
|
||||
declare function domToSvg<T extends Node>(context: Context<T>): Promise<string>;
|
||||
|
||||
declare function domToWebp<T extends Node>(node: T, options?: Options): Promise<string>;
|
||||
declare function domToWebp<T extends Node>(context: Context<T>): Promise<string>;
|
||||
|
||||
declare function createContext<T extends Node>(node: T, options?: Options & {
|
||||
autoDestruct?: boolean;
|
||||
}): Promise<Context<T>>;
|
||||
|
||||
declare function destroyContext(context: Context): void;
|
||||
|
||||
type Media = HTMLVideoElement | HTMLImageElement | SVGImageElement;
|
||||
interface LoadMediaOptions {
|
||||
ownerDocument?: Document;
|
||||
timeout?: number;
|
||||
onError?: (error: Error) => void;
|
||||
onWarn?: (...args: any[]) => void;
|
||||
}
|
||||
declare function loadMedia<T extends Media>(media: T, options?: LoadMediaOptions): Promise<T>;
|
||||
declare function loadMedia(media: string, options?: LoadMediaOptions): Promise<HTMLImageElement>;
|
||||
declare function waitUntilLoad(node: Node, options?: LoadMediaOptions): Promise<void>;
|
||||
|
||||
export { type Context, type Options, createContext, destroyContext, domToBlob, domToCanvas, domToDataUrl, domToForeignObjectSvg, domToImage, domToJpeg, domToPixel, domToPng, domToSvg, domToWebp, loadMedia, waitUntilLoad };
|
||||
1653
modules/modern-screenshot/4.6.7/dist/index.mjs
vendored
1653
modules/modern-screenshot/4.6.7/dist/index.mjs
vendored
File diff suppressed because it is too large
Load Diff
@@ -409,4 +409,3 @@ main "@solidjs/signals"
|
||||
main "@leeoniya/ufuzzy"
|
||||
main "lean-qr"
|
||||
main "lightweight-charts"
|
||||
main "modern-screenshot"
|
||||
|
||||
@@ -574,10 +574,6 @@
|
||||
color: var(--color);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
[data-screenshot="true"] &:has(input:not(:checked)) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
@@ -603,8 +599,7 @@
|
||||
right: 0;
|
||||
left: 0;
|
||||
text-transform: uppercase;
|
||||
margin: var(--main-padding);
|
||||
margin-bottom: var(--main-padding);
|
||||
margin: var(--main-padding) 0;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
justify-content: center;
|
||||
@@ -616,13 +611,18 @@
|
||||
}
|
||||
|
||||
> fieldset {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
min-width: 0;
|
||||
margin: -0.5rem 0;
|
||||
padding: 0.5rem var(--main-padding);
|
||||
pointer-events: auto;
|
||||
|
||||
> label,
|
||||
> button {
|
||||
pointer-events: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
> button {
|
||||
@@ -1018,19 +1018,6 @@
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
button#screenshot {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding-left: 1rem;
|
||||
height: 100%;
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-sm);
|
||||
|
||||
[data-screenshot="true"] & {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
> legend {
|
||||
gap: 1.5rem;
|
||||
|
||||
@@ -1039,10 +1026,6 @@
|
||||
}
|
||||
|
||||
> div {
|
||||
[data-screenshot="true"] &:has(input:not(:checked)) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
flex: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
@@ -1083,11 +1066,6 @@
|
||||
padding-right: 0.375rem;
|
||||
margin-left: -0.375rem;
|
||||
margin-right: -0.375rem;
|
||||
|
||||
[data-screenshot="true"] & {
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1129,6 +1107,16 @@
|
||||
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
button.capture {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
right: 0.5rem;
|
||||
z-index: 10;
|
||||
font-size: var(--font-size-xs);
|
||||
line-height: var(--line-height-xs);
|
||||
color: var(--off-color);
|
||||
}
|
||||
}
|
||||
|
||||
> .panes {
|
||||
@@ -1206,17 +1194,6 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
#interval {
|
||||
> span {
|
||||
display: none;
|
||||
|
||||
[data-screenshot="true"] & {
|
||||
color: var(--gray);
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .chart > legend,
|
||||
> fieldset {
|
||||
z-index: 20;
|
||||
@@ -1534,8 +1511,12 @@
|
||||
);
|
||||
|
||||
let storedTheme;
|
||||
try { storedTheme = localStorage.getItem("theme"); } catch (_) {}
|
||||
const isDark = storedTheme ? storedTheme === "dark" : preferredColorSchemeMatchMedia.matches;
|
||||
try {
|
||||
storedTheme = localStorage.getItem("theme");
|
||||
} catch (_) {}
|
||||
const isDark = storedTheme
|
||||
? storedTheme === "dark"
|
||||
: preferredColorSchemeMatchMedia.matches;
|
||||
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
|
||||
|
||||
const themeColor = window.document.createElement("meta");
|
||||
@@ -1801,6 +1782,8 @@
|
||||
</search>
|
||||
|
||||
<footer>
|
||||
<div class="shadow-left"></div>
|
||||
<div class="shadow-right"></div>
|
||||
<fieldset id="frame-selectors">
|
||||
<label
|
||||
id="aside-selector-label"
|
||||
@@ -1814,7 +1797,7 @@
|
||||
id="aside-selector"
|
||||
value="aside"
|
||||
/>
|
||||
Home
|
||||
View
|
||||
</label>
|
||||
|
||||
<label id="nav-selector-label" for="nav-selector" title="Nav">
|
||||
|
||||
@@ -1,37 +1,111 @@
|
||||
import { ios, canShare } from "../utils/env.js";
|
||||
import { domToBlob } from "../modules/modern-screenshot/4.6.7/dist/index.mjs";
|
||||
import { style } from "../utils/elements.js";
|
||||
import { colors } from "./colors.js";
|
||||
|
||||
export const canCapture = !ios || canShare;
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Element} args.element
|
||||
* @param {string} args.name
|
||||
* @param {HTMLCanvasElement} args.screenshot
|
||||
* @param {number} args.chartWidth
|
||||
* @param {HTMLElement} args.parent
|
||||
* @param {{ top: { element: HTMLElement }, bottom: { element: HTMLElement } }} args.legends
|
||||
*/
|
||||
export async function capture({ element, name }) {
|
||||
const blob = await domToBlob(element, {
|
||||
scale: 2,
|
||||
});
|
||||
export function capture({ screenshot, chartWidth, parent, legends }) {
|
||||
const dpr = screenshot.width / chartWidth;
|
||||
const pad = Math.round(16 * dpr);
|
||||
const fontSize = Math.round(14 * dpr);
|
||||
const titleFontSize = Math.round(20 * dpr);
|
||||
const circleRadius = Math.round(5 * dpr);
|
||||
const legendHeight = Math.round(28 * dpr);
|
||||
const titleHeight = Math.round(36 * dpr);
|
||||
|
||||
if (ios) {
|
||||
const file = new File(
|
||||
[blob],
|
||||
`bitview-${name}-${new Date().toJSON().split(".")[0]}.png`,
|
||||
{ type: "image/png" },
|
||||
);
|
||||
const title = (parent.querySelector("h1")?.textContent ?? "").toUpperCase();
|
||||
const hasTitle = title.length > 0;
|
||||
const hasTopLegend = legends.top.element.children.length > 0;
|
||||
const hasBottomLegend = legends.bottom.element.children.length > 0;
|
||||
const titleOffset = hasTitle ? titleHeight : 0;
|
||||
const topLegendOffset = hasTopLegend ? legendHeight : 0;
|
||||
const bottomOffset = hasBottomLegend ? legendHeight : 0;
|
||||
|
||||
try {
|
||||
await navigator.share({
|
||||
files: [file],
|
||||
title: `${name} on ${window.document.location.hostname}`,
|
||||
});
|
||||
return;
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = screenshot.width + pad * 2;
|
||||
canvas.height =
|
||||
screenshot.height + pad * 2 + titleOffset + topLegendOffset + bottomOffset;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
// Background
|
||||
const bodyBg = getComputedStyle(document.body).backgroundColor;
|
||||
const htmlBg = getComputedStyle(document.documentElement).backgroundColor;
|
||||
ctx.fillStyle = bodyBg === "rgba(0, 0, 0, 0)" ? htmlBg : bodyBg;
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
/** @param {HTMLElement} legendEl @param {number} y */
|
||||
const drawLegend = (legendEl, y) => {
|
||||
ctx.font = `${fontSize}px ${style.fontFamily}`;
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "middle";
|
||||
let x = pad;
|
||||
for (const div of legendEl.children) {
|
||||
const label = div.querySelector("label");
|
||||
if (!label) continue;
|
||||
const input = label.querySelector("input");
|
||||
if (input && !input.checked) continue;
|
||||
// Draw color circles
|
||||
const colorSpans = label.querySelectorAll(".colors span");
|
||||
for (const span of colorSpans) {
|
||||
ctx.fillStyle = /** @type {HTMLElement} */ (span).style.backgroundColor;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x + circleRadius, y, circleRadius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
x += circleRadius * 2 + Math.round(2 * dpr);
|
||||
}
|
||||
// Draw name
|
||||
const name = label.querySelector(".name")?.textContent ?? "";
|
||||
ctx.fillStyle = colors.default();
|
||||
ctx.fillText(name, x + Math.round(4 * dpr), y);
|
||||
x += ctx.measureText(name).width + Math.round(20 * dpr);
|
||||
}
|
||||
};
|
||||
|
||||
// Title
|
||||
if (hasTitle) {
|
||||
ctx.font = `${titleFontSize}px ${style.fontFamily}`;
|
||||
ctx.fillStyle = colors.default();
|
||||
ctx.textAlign = "left";
|
||||
ctx.textBaseline = "middle";
|
||||
ctx.fillText(title, pad, pad + titleHeight / 2);
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, "_blank");
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
// Top legend
|
||||
if (hasTopLegend) {
|
||||
drawLegend(legends.top.element, pad + titleOffset + topLegendOffset / 2);
|
||||
}
|
||||
|
||||
// Chart
|
||||
ctx.drawImage(screenshot, pad, pad + titleOffset + topLegendOffset);
|
||||
|
||||
// Bottom legend
|
||||
if (hasBottomLegend) {
|
||||
drawLegend(
|
||||
legends.bottom.element,
|
||||
pad + titleOffset + topLegendOffset + screenshot.height + legendHeight / 2,
|
||||
);
|
||||
}
|
||||
|
||||
// Watermark
|
||||
ctx.fillStyle = colors.gray();
|
||||
ctx.font = `${fontSize}px ${style.fontFamily}`;
|
||||
ctx.textAlign = "right";
|
||||
ctx.textBaseline = "bottom";
|
||||
ctx.fillText(window.location.host, canvas.width - pad, canvas.height - pad / 2);
|
||||
|
||||
// Open in new tab
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return;
|
||||
const url = URL.createObjectURL(blob);
|
||||
window.open(url, "_blank");
|
||||
setTimeout(() => URL.revokeObjectURL(url), 100);
|
||||
}, "image/png");
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
// } from "../modules/lightweight-charts/5.1.0/dist/lightweight-charts.standalone.development.mjs";
|
||||
} from "../modules/lightweight-charts/5.1.0/dist/lightweight-charts.standalone.production.mjs";
|
||||
import { createLegend } from "./legend.js";
|
||||
import { capture, canCapture } from "./capture.js";
|
||||
import { capture } from "./capture.js";
|
||||
import { colors } from "./colors.js";
|
||||
import { createChoiceField } from "../utils/dom.js";
|
||||
import { createPersistedValue } from "../utils/persisted.js";
|
||||
@@ -76,14 +76,12 @@ const lineWidth = /** @type {any} */ (1.5);
|
||||
* @param {HTMLElement} args.parent
|
||||
* @param {BrkClient} args.brk
|
||||
* @param {true} [args.fitContent]
|
||||
* @param {HTMLElement} [args.captureElement]
|
||||
*/
|
||||
export function createChart({
|
||||
parent,
|
||||
id: chartId,
|
||||
brk,
|
||||
fitContent,
|
||||
captureElement,
|
||||
}) {
|
||||
const baseUrl = brk.baseUrl.replace(/\/$/, "");
|
||||
|
||||
@@ -1513,33 +1511,19 @@ export function createChart({
|
||||
},
|
||||
};
|
||||
|
||||
if (captureElement && canCapture) {
|
||||
const domain = document.createElement("p");
|
||||
domain.innerText = window.location.host;
|
||||
domain.id = "domain";
|
||||
|
||||
fieldsets.addIfNeeded({
|
||||
id: "capture",
|
||||
paneIndex: 0,
|
||||
position: "ne",
|
||||
createChild() {
|
||||
const button = document.createElement("button");
|
||||
button.id = "capture";
|
||||
button.innerText = "capture";
|
||||
button.title = "Capture chart as image";
|
||||
button.addEventListener("click", async () => {
|
||||
captureElement.dataset.screenshot = "true";
|
||||
captureElement.append(domain);
|
||||
try {
|
||||
await capture({ element: captureElement, name: chartId });
|
||||
} catch {}
|
||||
captureElement.removeChild(domain);
|
||||
captureElement.dataset.screenshot = "false";
|
||||
});
|
||||
return button;
|
||||
},
|
||||
const captureButton = document.createElement("button");
|
||||
captureButton.className = "capture";
|
||||
captureButton.innerText = "capture";
|
||||
captureButton.title = "Capture chart as image";
|
||||
captureButton.addEventListener("click", () => {
|
||||
capture({
|
||||
screenshot: ichart.takeScreenshot(),
|
||||
chartWidth: elements.chart.clientWidth,
|
||||
parent,
|
||||
legends,
|
||||
});
|
||||
}
|
||||
});
|
||||
elements.chart.append(captureButton);
|
||||
|
||||
return chart;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import signals from "./signals.js";
|
||||
import { BrkClient } from "./modules/brk-client/index.js";
|
||||
import { initOptions } from "./options/full.js";
|
||||
import ufuzzy from "./modules/leeoniya-ufuzzy/1.0.19/dist/uFuzzy.mjs";
|
||||
import * as leanQr from "./modules/lean-qr/2.7.1/index.mjs";
|
||||
import { init as initExplorer } from "./panes/_explorer.js";
|
||||
import { init as initChart } from "./panes/chart.js";
|
||||
import { init as initTable } from "./panes/table.js";
|
||||
import { init as initSimulation } from "./panes/_simulation.js";
|
||||
import { next } from "./utils/timing.js";
|
||||
import { replaceHistory } from "./utils/url.js";
|
||||
import { removeStored, writeToStorage } from "./utils/storage.js";
|
||||
@@ -18,7 +14,6 @@ import {
|
||||
asideLabelElement,
|
||||
bodyElement,
|
||||
chartElement,
|
||||
explorerElement,
|
||||
frameSelectorsElement,
|
||||
mainElement,
|
||||
navElement,
|
||||
@@ -26,9 +21,7 @@ import {
|
||||
searchElement,
|
||||
searchInput,
|
||||
searchResultsElement,
|
||||
simulationElement,
|
||||
style,
|
||||
tableElement,
|
||||
} from "./utils/elements.js";
|
||||
|
||||
function initFrameSelectors() {
|
||||
@@ -120,13 +113,9 @@ signals.createRoot(() => {
|
||||
|
||||
console.log(`VERSION = ${brk.VERSION}`);
|
||||
|
||||
const qrcode = signals.createSignal(/** @type {string | null} */ (null));
|
||||
|
||||
signals.createEffect(webSockets.kraken1dCandle.latest, (latest) => {
|
||||
if (latest) {
|
||||
console.log("close:", latest.close);
|
||||
window.document.title = `${latest.close.toLocaleString("en-us")} | ${window.location.host}`;
|
||||
}
|
||||
webSockets.kraken1dCandle.onLatest((latest) => {
|
||||
console.log("close:", latest.close);
|
||||
window.document.title = `${latest.close.toLocaleString("en-us")} | ${window.location.host}`;
|
||||
});
|
||||
|
||||
// function createLastHeightResource() {
|
||||
@@ -149,7 +138,6 @@ signals.createRoot(() => {
|
||||
const options = initOptions({
|
||||
signals,
|
||||
brk,
|
||||
qrcode,
|
||||
});
|
||||
|
||||
window.addEventListener("popstate", (_event) => {
|
||||
@@ -185,31 +173,15 @@ signals.createRoot(() => {
|
||||
const chartOption = signals.createSignal(
|
||||
/** @type {ChartOption | null} */ (null),
|
||||
);
|
||||
const simOption = signals.createSignal(
|
||||
/** @type {SimulationOption | null} */ (null),
|
||||
);
|
||||
|
||||
let previousElement = /** @type {HTMLElement | undefined} */ (undefined);
|
||||
let firstTimeLoadingChart = true;
|
||||
let firstTimeLoadingTable = true;
|
||||
let firstTimeLoadingSimulation = true;
|
||||
let firstTimeLoadingExplorer = true;
|
||||
|
||||
signals.createScopedEffect(options.selected, (option) => {
|
||||
/** @type {HTMLElement} */
|
||||
/** @type {HTMLElement | undefined} */
|
||||
let element;
|
||||
|
||||
switch (option.kind) {
|
||||
case "explorer": {
|
||||
element = explorerElement;
|
||||
|
||||
if (firstTimeLoadingExplorer) {
|
||||
signals.runWithOwner(owner, () => initExplorer());
|
||||
}
|
||||
firstTimeLoadingExplorer = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case "chart": {
|
||||
element = chartElement;
|
||||
|
||||
@@ -227,33 +199,13 @@ signals.createRoot(() => {
|
||||
|
||||
break;
|
||||
}
|
||||
case "table": {
|
||||
element = tableElement;
|
||||
|
||||
if (firstTimeLoadingTable) {
|
||||
signals.runWithOwner(owner, () => initTable());
|
||||
}
|
||||
firstTimeLoadingTable = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case "simulation": {
|
||||
element = simulationElement;
|
||||
|
||||
simOption.set(option);
|
||||
|
||||
if (firstTimeLoadingSimulation) {
|
||||
signals.runWithOwner(owner, () => initSimulation());
|
||||
}
|
||||
firstTimeLoadingSimulation = false;
|
||||
|
||||
break;
|
||||
}
|
||||
case "url": {
|
||||
case "link": {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!element) throw "Element should be set";
|
||||
|
||||
if (element !== previousElement) {
|
||||
if (previousElement) previousElement.hidden = true;
|
||||
element.hidden = false;
|
||||
@@ -505,7 +457,6 @@ signals.createRoot(() => {
|
||||
const element = options.createOptionElement({
|
||||
option,
|
||||
name: title,
|
||||
qrcode,
|
||||
});
|
||||
|
||||
if (element) {
|
||||
@@ -522,60 +473,6 @@ signals.createRoot(() => {
|
||||
searchInput.addEventListener("input", inputEvent);
|
||||
});
|
||||
|
||||
function initShare() {
|
||||
const shareDiv = getElementById("share-div");
|
||||
const shareContentDiv = getElementById("share-content-div");
|
||||
const shareButton = getElementById("share-button");
|
||||
|
||||
shareButton.addEventListener("click", () => {
|
||||
qrcode.set(window.location.href);
|
||||
});
|
||||
|
||||
|
||||
shareDiv.addEventListener("click", () => {
|
||||
qrcode.set(null);
|
||||
});
|
||||
|
||||
shareContentDiv.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
signals.runWithOwner(owner, () => {
|
||||
const imgQrcode = /** @type {HTMLImageElement} */ (
|
||||
getElementById("share-img")
|
||||
);
|
||||
|
||||
const anchor = /** @type {HTMLAnchorElement} */ (
|
||||
getElementById("share-anchor")
|
||||
);
|
||||
|
||||
signals.createEffect(qrcode, (qrcode) => {
|
||||
if (!qrcode) {
|
||||
shareDiv.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const href = qrcode;
|
||||
anchor.href = href;
|
||||
anchor.innerText =
|
||||
(href.startsWith("http")
|
||||
? href.split("//").at(-1)
|
||||
: href.split(":").at(-1)) || "";
|
||||
|
||||
imgQrcode.src =
|
||||
leanQr.generate(/** @type {any} */ (href))?.toDataURL({
|
||||
// @ts-ignore
|
||||
padX: 0,
|
||||
padY: 0,
|
||||
}) || "";
|
||||
|
||||
shareDiv.hidden = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
initShare();
|
||||
|
||||
function initDesktopResizeBar() {
|
||||
const resizeBar = getElementById("resize-bar");
|
||||
let resize = false;
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
import { createPartialOptions } from "./partial.js";
|
||||
import {
|
||||
createButtonElement,
|
||||
createAnchorElement,
|
||||
} from "../utils/dom.js";
|
||||
import { createButtonElement, createAnchorElement } from "../utils/dom.js";
|
||||
import { pushHistory, resetParams } from "../utils/url.js";
|
||||
import { readStored, writeToStorage } from "../utils/storage.js";
|
||||
import { stringToId } from "../utils/format.js";
|
||||
import { collect, markUsed, logUnused } from "./unused.js";
|
||||
import { setQr } from "../panes/share.js";
|
||||
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Signals} args.signals
|
||||
* @param {BrkClient} args.brk
|
||||
* @param {Signal<string | null>} args.qrcode
|
||||
*/
|
||||
export function initOptions({ signals, brk, qrcode }) {
|
||||
export function initOptions({ signals, brk }) {
|
||||
collect(brk.metrics);
|
||||
|
||||
const LS_SELECTED_KEY = `selected_path`;
|
||||
@@ -93,12 +90,11 @@ export function initOptions({ signals, brk, qrcode }) {
|
||||
/**
|
||||
* @param {Object} args
|
||||
* @param {Option} args.option
|
||||
* @param {Signal<string | null>} args.qrcode
|
||||
* @param {string} [args.name]
|
||||
*/
|
||||
function createOptionElement({ option, name, qrcode }) {
|
||||
function createOptionElement({ option, name }) {
|
||||
const title = option.title;
|
||||
if (option.kind === "url") {
|
||||
if (option.kind === "link") {
|
||||
const href = option.url();
|
||||
|
||||
if (option.qrcode) {
|
||||
@@ -106,7 +102,7 @@ export function initOptions({ signals, brk, qrcode }) {
|
||||
inside: option.name,
|
||||
title,
|
||||
onClick: () => {
|
||||
qrcode.set(option.url);
|
||||
setQr(option.url());
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -215,7 +211,7 @@ export function initOptions({ signals, brk, qrcode }) {
|
||||
Object.assign(
|
||||
option,
|
||||
/** @satisfies {UrlOption} */ ({
|
||||
kind: "url",
|
||||
kind: "link",
|
||||
path,
|
||||
name,
|
||||
title: name,
|
||||
@@ -318,7 +314,6 @@ export function initOptions({ signals, brk, qrcode }) {
|
||||
} else {
|
||||
const element = createOptionElement({
|
||||
option: node.option,
|
||||
qrcode,
|
||||
});
|
||||
li.append(element);
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
* @typedef {Required<PartialSimulationOption> & ProcessedOptionAddons} SimulationOption
|
||||
*
|
||||
* @typedef {Object} PartialUrlOptionSpecific
|
||||
* @property {"url"} [kind]
|
||||
* @property {"link"} [kind]
|
||||
* @property {() => string} url
|
||||
* @property {string} title
|
||||
* @property {boolean} [qrcode]
|
||||
|
||||
@@ -29,7 +29,6 @@ export function init({ option, brk }) {
|
||||
parent: chartElement,
|
||||
id: "charts",
|
||||
brk,
|
||||
captureElement: chartElement,
|
||||
});
|
||||
|
||||
// Create index selector using chart's index state
|
||||
@@ -104,10 +103,7 @@ export function init({ option, brk }) {
|
||||
});
|
||||
|
||||
// Live price update listener
|
||||
signals.createEffect(
|
||||
() => webSockets.kraken1dCandle.latest(),
|
||||
updatePriceWithLatest,
|
||||
);
|
||||
webSockets.kraken1dCandle.onLatest(updatePriceWithLatest);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,10 +150,6 @@ function createIndexSelector(option, chart) {
|
||||
fieldset.id = "interval";
|
||||
fieldset.dataset.size = "sm";
|
||||
|
||||
const screenshotSpan = window.document.createElement("span");
|
||||
screenshotSpan.innerText = "interval:";
|
||||
fieldset.append(screenshotSpan);
|
||||
|
||||
// Track user's preferred index (only updated on explicit selection)
|
||||
let preferredIndex = chart.index.name.value;
|
||||
|
||||
|
||||
45
website/scripts/panes/share.js
Normal file
45
website/scripts/panes/share.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import { getElementById } from "../utils/dom.js";
|
||||
import * as leanQr from "../modules/lean-qr/2.7.1/index.mjs";
|
||||
|
||||
const shareDiv = getElementById("share-div");
|
||||
const shareContentDiv = getElementById("share-content-div");
|
||||
const shareButton = getElementById("share-button");
|
||||
const imgQrcode = /** @type {HTMLImageElement} */ (getElementById("share-img"));
|
||||
const anchor = /** @type {HTMLAnchorElement} */ (
|
||||
getElementById("share-anchor")
|
||||
);
|
||||
|
||||
/** @param {string | null} url */
|
||||
export function setQr(url) {
|
||||
if (!url) {
|
||||
shareDiv.hidden = true;
|
||||
return;
|
||||
}
|
||||
|
||||
anchor.href = url;
|
||||
anchor.innerText =
|
||||
(url.startsWith("http") ? url.split("//").at(-1) : url.split(":").at(-1)) ||
|
||||
"";
|
||||
|
||||
imgQrcode.src =
|
||||
leanQr.generate(/** @type {any} */ (url))?.toDataURL({
|
||||
// @ts-ignore
|
||||
padX: 0,
|
||||
padY: 0,
|
||||
}) || "";
|
||||
|
||||
shareDiv.hidden = false;
|
||||
}
|
||||
|
||||
shareButton.addEventListener("click", () => {
|
||||
setQr(window.location.href);
|
||||
});
|
||||
|
||||
shareDiv.addEventListener("click", () => {
|
||||
setQr(null);
|
||||
});
|
||||
|
||||
shareContentDiv.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
@@ -212,149 +212,6 @@ export function importStyle(href) {
|
||||
return link;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Object} args
|
||||
* @param {T} args.defaultValue - Fallback when selected value is no longer in choices
|
||||
* @param {string} [args.id]
|
||||
* @param {readonly T[] | Accessor<readonly T[]>} args.choices
|
||||
* @param {boolean} [args.sorted]
|
||||
* @param {Signals} args.signals
|
||||
* @param {Signal<T>} args.selected
|
||||
* @param {(choice: T) => string} [args.toKey] - Extract string key (defaults to identity for strings)
|
||||
* @param {(choice: T) => string} [args.toLabel] - Extract display label (defaults to identity for strings)
|
||||
* @param {"radio" | "select"} [args.type] - Render as radio buttons or select dropdown
|
||||
*/
|
||||
export function createReactiveChoiceField({
|
||||
id,
|
||||
choices: unsortedChoices,
|
||||
defaultValue,
|
||||
signals,
|
||||
selected,
|
||||
sorted,
|
||||
toKey = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
||||
toLabel = /** @type {(choice: T) => string} */ ((/** @type {any} */ c) => c),
|
||||
type = /** @type {const} */ ("radio"),
|
||||
}) {
|
||||
const defaultKey = toKey(defaultValue);
|
||||
|
||||
const choices = signals.createMemo(() => {
|
||||
/** @type {readonly T[]} */
|
||||
let c;
|
||||
if (typeof unsortedChoices === "function") {
|
||||
c = unsortedChoices();
|
||||
} else {
|
||||
c = unsortedChoices;
|
||||
}
|
||||
|
||||
return sorted
|
||||
? /** @type {readonly T[]} */ (
|
||||
/** @type {any} */ (
|
||||
c.toSorted((a, b) => toLabel(a).localeCompare(toLabel(b)))
|
||||
)
|
||||
)
|
||||
: c;
|
||||
});
|
||||
|
||||
/** @param {string} key */
|
||||
const fromKey = (key) =>
|
||||
choices().find((c) => toKey(c) === key) ?? defaultValue;
|
||||
|
||||
const field = window.document.createElement("div");
|
||||
field.classList.add("field");
|
||||
|
||||
const div = window.document.createElement("div");
|
||||
field.append(div);
|
||||
|
||||
/** @type {HTMLElement | null} */
|
||||
let remainingSmall = null;
|
||||
if (type === "select") {
|
||||
remainingSmall = window.document.createElement("small");
|
||||
field.append(remainingSmall);
|
||||
}
|
||||
|
||||
signals.createScopedEffect(choices, (choices) => {
|
||||
const s = selected();
|
||||
const sKey = toKey(s);
|
||||
const keys = choices.map(toKey);
|
||||
if (!keys.includes(sKey)) {
|
||||
if (keys.includes(defaultKey)) {
|
||||
selected.set(() => defaultValue);
|
||||
} else if (choices.length) {
|
||||
selected.set(() => choices[0]);
|
||||
}
|
||||
}
|
||||
|
||||
div.innerHTML = "";
|
||||
|
||||
if (choices.length === 1) {
|
||||
const span = window.document.createElement("span");
|
||||
span.textContent = toLabel(choices[0]);
|
||||
div.append(span);
|
||||
|
||||
if (remainingSmall) {
|
||||
remainingSmall.hidden = true;
|
||||
}
|
||||
} else if (type === "select") {
|
||||
const select = window.document.createElement("select");
|
||||
select.id = id ?? "";
|
||||
select.name = id ?? "";
|
||||
|
||||
choices.forEach((choice) => {
|
||||
const option = window.document.createElement("option");
|
||||
option.value = toKey(choice);
|
||||
option.textContent = toLabel(choice);
|
||||
if (toKey(choice) === sKey) {
|
||||
option.selected = true;
|
||||
}
|
||||
select.append(option);
|
||||
});
|
||||
|
||||
select.addEventListener("change", () => {
|
||||
selected.set(() => fromKey(select.value));
|
||||
});
|
||||
|
||||
div.append(select);
|
||||
|
||||
if (remainingSmall) {
|
||||
const remaining = choices.length - 1;
|
||||
if (remaining > 0) {
|
||||
remainingSmall.textContent = ` +${remaining}`;
|
||||
remainingSmall.hidden = false;
|
||||
} else {
|
||||
remainingSmall.hidden = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const fieldId = id ?? "";
|
||||
choices.forEach((choice) => {
|
||||
const choiceKey = toKey(choice);
|
||||
const choiceLabel = toLabel(choice);
|
||||
const { label } = createLabeledInput({
|
||||
inputId: `${fieldId}-${choiceKey.toLowerCase()}`,
|
||||
inputName: fieldId,
|
||||
inputValue: choiceKey,
|
||||
inputChecked: choiceKey === sKey,
|
||||
// title: choiceLabel,
|
||||
type: "radio",
|
||||
});
|
||||
|
||||
const text = window.document.createTextNode(choiceLabel);
|
||||
label.append(text);
|
||||
div.append(label);
|
||||
});
|
||||
|
||||
field.addEventListener("change", (event) => {
|
||||
// @ts-ignore
|
||||
const value = event.target.value;
|
||||
selected.set(() => fromKey(value));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Object} args
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import signals from "../signals.js";
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {(callback: (value: T) => void) => WebSocket} creator
|
||||
*/
|
||||
function createWebsocket(creator) {
|
||||
let ws = /** @type {WebSocket | null} */ (null);
|
||||
let _live = false;
|
||||
let _latest = /** @type {T | null} */ (null);
|
||||
|
||||
const live = signals.createSignal(false);
|
||||
const latest = signals.createSignal(/** @type {T | null} */ (null));
|
||||
/** @type {Set<(value: T) => void>} */
|
||||
const listeners = new Set();
|
||||
|
||||
function reinitWebSocket() {
|
||||
if (!ws || ws.readyState === ws.CLOSED) {
|
||||
@@ -22,19 +22,31 @@ function createWebsocket(creator) {
|
||||
}
|
||||
|
||||
const resource = {
|
||||
live,
|
||||
latest,
|
||||
live() {
|
||||
return _live;
|
||||
},
|
||||
latest() {
|
||||
return _latest;
|
||||
},
|
||||
/** @param {(value: T) => void} callback */
|
||||
onLatest(callback) {
|
||||
listeners.add(callback);
|
||||
return () => listeners.delete(callback);
|
||||
},
|
||||
open() {
|
||||
ws = creator((value) => latest.set(() => value));
|
||||
ws = creator((value) => {
|
||||
_latest = value;
|
||||
listeners.forEach((cb) => cb(value));
|
||||
});
|
||||
|
||||
ws.addEventListener("open", () => {
|
||||
console.log("ws: open");
|
||||
live.set(true);
|
||||
_live = true;
|
||||
});
|
||||
|
||||
ws.addEventListener("close", () => {
|
||||
console.log("ws: close");
|
||||
live.set(false);
|
||||
_live = false;
|
||||
});
|
||||
|
||||
window.document.addEventListener(
|
||||
@@ -51,7 +63,7 @@ function createWebsocket(creator) {
|
||||
reinitWebSocketIfDocumentNotHidden,
|
||||
);
|
||||
window.document.removeEventListener("online", reinitWebSocket);
|
||||
live.set(false);
|
||||
_live = false;
|
||||
ws = null;
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user