Convert a selected WME segment into a River / Stream area venue.
// ==UserScript==
// @name WME StR Simplified
// @namespace https://greasyfork.org/en/users/1614344-alanoftheberg
// @version 2026.06.22.03
// @description Convert a selected WME segment into a River / Stream area venue.
// @author AlanOfTheBerg
// @match https://www.waze.com/*editor*
// @match https://beta.waze.com/*editor*
// @grant GM_xmlhttpRequest
// @grant GM_info
// @grant unsafeWindow
// @connect update.greasyfork.org
// @connect greasyfork.org
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// Granting GM_xmlhttpRequest moves Tampermonkey execution into a sandbox.
// Bridge WME page globals back into that sandbox so the script can still see WME SDK globals.
const WME_PAGE = (typeof unsafeWindow !== "undefined") ? unsafeWindow : window;
["getWmeSdk", "SDK_INITIALIZED"].forEach(function (key) {
try {
Object.defineProperty(window, key, {
configurable: true,
enumerable: false,
get: function () { return WME_PAGE[key]; }
});
} catch (ignored) {}
});
function getInstalledScriptVersion() {
try {
if (typeof GM_info !== "undefined" && GM_info.script && GM_info.script.version) {
return GM_info.script.version;
}
} catch (ignored) {}
return "unknown";
}
const SCRIPT_ID = "wme-str-street-to-river-simplified";
const SCRIPT_NAME = "WME StR Simplified";
const SCRIPT_VERSION = getInstalledScriptVersion();
const DEFAULT_WIDTH_METERS = 12;
const DEFAULT_MITER_LIMIT = 4;
const IDS = {
root: "wme-str-root",
selectedPanel: "wme-str-selected-panel",
noSelectionPanel: "wme-str-no-selection-panel",
widthInput: "wme-str-width-input",
deleteCheckbox: "wme-str-delete-checkbox",
riverButton: "wme-str-river-button",
canalButton: "wme-str-canal-button",
status: "wme-str-status",
selectedSegment: "wme-str-selected-segment",
selectedSegmentLength: "wme-str-selected-segment-length",
selectedSegmentVertices: "wme-str-selected-segment-vertices"
};
let wmeSDK = null;
let ui = null;
waitForWmeSdk()
.then((sdk) => {
wmeSDK = sdk;
return initializeScript();
})
.catch((error) => {
console.error(`${SCRIPT_NAME}: failed to initialize`, error);
});
function waitForWmeSdk() {
return new Promise((resolve, reject) => {
const timeoutMs = 30000;
const startedAt = Date.now();
const check = () => {
try {
if (
window.SDK_INITIALIZED &&
typeof window.SDK_INITIALIZED.then === "function" &&
typeof window.getWmeSdk === "function"
) {
window.SDK_INITIALIZED
.then(() => {
const sdk = window.getWmeSdk({
scriptId: SCRIPT_ID,
scriptName: SCRIPT_NAME
});
resolve(sdk);
})
.catch(reject);
return;
}
if (Date.now() - startedAt > timeoutMs) {
reject(new Error("Timed out waiting for WME SDK. Make sure this is running in WME."));
return;
}
window.setTimeout(check, 250);
} catch (error) {
reject(error);
}
};
check();
});
}
async function initializeScript() {
injectStyles();
await createSidebarTab();
wmeSDK.Events.on({
eventName: "wme-selection-changed",
eventHandler: updateUiForSelection
});
updateUiForSelection();
console.log(`${SCRIPT_NAME}: initialized`);
}
async function createSidebarTab() {
const section = document.createElement("section");
section.id = IDS.root;
section.innerHTML = `
<div class="str-header">
<h3>StR Simplified</h3>
<div class="str-version">VERSION ${SCRIPT_VERSION}</div>
<p>Converts a selected segment into a River / Stream area venue.</p>
</div>
<div id="${IDS.noSelectionPanel}" class="str-card">
<strong>No segment selected</strong>
<p>Select exactly one segment to enable the conversion button.</p>
</div>
<div id="${IDS.selectedPanel}" class="str-card" style="display:none;">
<div class="str-field">
<label>Selected segment</label>
<div class="str-detail-grid">
<div class="str-detail-label">ID</div>
<div id="${IDS.selectedSegment}" class="str-detail-value"></div>
<div class="str-detail-label">Length</div>
<div id="${IDS.selectedSegmentLength}" class="str-detail-value"></div>
<div class="str-detail-label">Vertices</div>
<div id="${IDS.selectedSegmentVertices}" class="str-detail-value"></div>
</div>
</div>
<div class="str-field">
<label for="${IDS.widthInput}">River width, meters</label>
<input
id="${IDS.widthInput}"
type="number"
min="1"
step="1"
value="${DEFAULT_WIDTH_METERS}"
/>
</div>
<label class="str-checkbox">
<input id="${IDS.deleteCheckbox}" type="checkbox" />
Delete source segment after creating venue
</label>
<div class="str-button-row">
<button id="${IDS.riverButton}" type="button">Make river</button>
<button id="${IDS.canalButton}" type="button">Make canal</button>
</div>
<div id="${IDS.status}" class="str-status"></div>
</div>
`;
const { tabLabel, tabPane } = await wmeSDK.Sidebar.registerScriptTab();
tabLabel.textContent = "StR";
tabLabel.title = SCRIPT_NAME;
tabPane.appendChild(section);
ui = {
root: section,
noSelectionPanel: section.querySelector(`#${IDS.noSelectionPanel}`),
selectedPanel: section.querySelector(`#${IDS.selectedPanel}`),
selectedSegment: section.querySelector(`#${IDS.selectedSegment}`),
selectedSegmentLength: section.querySelector(`#${IDS.selectedSegmentLength}`),
selectedSegmentVertices: section.querySelector(`#${IDS.selectedSegmentVertices}`),
widthInput: section.querySelector(`#${IDS.widthInput}`),
deleteCheckbox: section.querySelector(`#${IDS.deleteCheckbox}`),
riverButton: section.querySelector(`#${IDS.riverButton}`),
canalButton: section.querySelector(`#${IDS.canalButton}`),
status: section.querySelector(`#${IDS.status}`)
};
ui.riverButton.addEventListener("click", () => handleStreetToWaterClick("river_stream"));
ui.canalButton.addEventListener("click", () => handleStreetToWaterClick("canal"));
}
function updateUiForSelection() {
if (!ui) return;
const selectedSegment = tryGetSingleSelectedSegment();
if (!selectedSegment) {
ui.noSelectionPanel.style.display = "block";
ui.selectedPanel.style.display = "none";
ui.status.textContent = "";
return;
}
ui.noSelectionPanel.style.display = "none";
ui.selectedPanel.style.display = "block";
ui.selectedSegment.textContent = String(selectedSegment.id);
ui.selectedSegmentLength.textContent = formatSegmentLength(selectedSegment);
ui.selectedSegmentVertices.textContent = String(getSegmentVertexCount(selectedSegment));
ui.status.textContent = "";
}
function setStatus(message, type = "info") {
if (!ui || !ui.status) return;
ui.status.textContent = message;
ui.status.className = `str-status str-status-${type}`;
}
function setActionButtonsDisabled(disabled) {
if (!ui) return;
if (ui.riverButton) ui.riverButton.disabled = disabled;
if (ui.canalButton) ui.canalButton.disabled = disabled;
}
async function handleStreetToWaterClick(categoryType) {
if (!ui) return;
try {
setActionButtonsDisabled(true);
setStatus(`Creating ${getCategoryDisplayName(categoryType)} area...`, "info");
const widthMeters = Number(ui.widthInput.value);
const deleteOriginalSegment = Boolean(ui.deleteCheckbox.checked);
if (!Number.isFinite(widthMeters) || widthMeters <= 0) {
throw new Error("Enter a valid positive width in meters.");
}
const result = createWaterAreaFromSelectedSegment({
widthMeters,
deleteOriginalSegment,
categoryType
});
setStatus(`Created ${getCategoryDisplayName(categoryType)} venue ${result.venueId}. Save your edits when ready.`, "success");
console.log(`${SCRIPT_NAME}: created ${getCategoryDisplayName(categoryType)} venue`, result);
updateUiForSelection();
} catch (error) {
console.error(`${SCRIPT_NAME}: conversion failed`, error);
setStatus(error.message || String(error), "error");
} finally {
setActionButtonsDisabled(false);
}
}
function injectStyles() {
const styleId = "wme-str-style";
if (document.getElementById(styleId)) return;
const css = `
#${IDS.root} {
padding: 22px 10px 10px 10px;
font-family: inherit;
font-size: 13px;
line-height: 1.35;
color: inherit;
}
#${IDS.root} .str-header {
margin: 0 0 10px 0;
}
#${IDS.root} .str-header h3 {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: 600;
line-height: 1.25;
}
#${IDS.root} .str-version {
margin: 0px 0 8px 0;
font-size: 10px;
line-height: 1.2;
color: #777;
font-weight: 600;
letter-spacing: 0.6px;
}
#${IDS.root} .str-header p {
margin: 8px 0 0 0;
font-size: 12px;
line-height: 1.35;
opacity: 0.75;
}
#${IDS.root} .str-card {
padding: 0;
margin: 0 0 12px 0;
background: transparent;
border: 0;
}
#${IDS.root} .str-card p {
margin: 4px 0 0 0;
font-size: 12px;
line-height: 1.35;
opacity: 0.75;
}
#${IDS.root} .str-field {
margin-bottom: 12px;
}
#${IDS.root} .str-field > label {
display: block;
margin-bottom: 4px;
font-size: 12px;
font-weight: 600;
line-height: 1.25;
}
#${IDS.root} input[type="number"] {
width: 100%;
box-sizing: border-box;
min-height: 30px;
padding: 4px 8px;
font: inherit;
}
#${IDS.root} .str-detail-grid {
display: grid;
grid-template-columns: 72px 1fr;
gap: 4px 8px;
padding: 8px;
border-radius: 4px;
background: rgba(0, 0, 0, 0.04);
}
#${IDS.root} .str-detail-label {
font-size: 12px;
font-weight: 600;
opacity: 0.75;
}
#${IDS.root} .str-detail-value {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
font-size: 12px;
overflow-wrap: anywhere;
}
#${IDS.root} .str-checkbox {
display: flex;
align-items: flex-start;
gap: 6px;
margin: 4px 0 12px 0;
font-size: 12px;
line-height: 1.35;
}
#${IDS.root} .str-checkbox input {
margin-top: 2px;
}
#${IDS.root} .str-button-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
margin-top: 4px;
}
#${IDS.root} .str-button-row button {
min-height: 32px;
padding: 6px 8px;
border: 1px solid #0078d4;
border-radius: 4px;
background: #0078d4;
color: #fff;
font: inherit;
font-weight: 600;
cursor: pointer;
}
#${IDS.root} .str-button-row button:hover:not(:disabled),
#${IDS.root} .str-button-row button:focus:not(:disabled) {
background: #106ebe;
border-color: #106ebe;
}
#${IDS.root} .str-button-row button:disabled {
opacity: 0.6;
cursor: default;
}
#${IDS.root} .str-status {
margin-top: 10px;
font-size: 12px;
line-height: 1.35;
}
#${IDS.root} .str-status-info {
opacity: 0.75;
}
#${IDS.root} .str-status-success {
color: #107c10;
}
#${IDS.root} .str-status-error {
color: #a80000;
}
`;
if ("adoptedStyleSheets" in document && "replaceSync" in CSSStyleSheet.prototype) {
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
const marker = document.createElement("meta");
marker.id = styleId;
marker.dataset.source = SCRIPT_ID;
document.head.appendChild(marker);
return;
}
const style = document.createElement("style");
style.id = styleId;
style.textContent = css;
document.head.appendChild(style);
}
function createWaterAreaFromSelectedSegment({
widthMeters,
deleteOriginalSegment,
categoryType
}) {
const selectedSegment = getSingleSelectedSegment();
const venueCategoryId = getVenueCategoryId(categoryType);
const polygon = segmentToCenteredAreaPolygon(selectedSegment, widthMeters, {
miterLimit: DEFAULT_MITER_LIMIT
});
const venueId = wmeSDK.DataModel.Venues.addVenue({
category: venueCategoryId,
geometry: polygon
});
if (deleteOriginalSegment) {
wmeSDK.DataModel.Segments.deleteSegment({
segmentId: selectedSegment.id
});
}
return {
sourceSegmentId: selectedSegment.id,
venueId,
category: venueCategoryId,
deletedSourceSegment: deleteOriginalSegment,
polygon
};
}
function getSingleSelectedSegment() {
const segment = tryGetSingleSelectedSegment();
if (!segment) {
throw new Error("Select exactly one segment first.");
}
return segment;
}
function tryGetSingleSelectedSegment() {
const selection = wmeSDK.Editing.getSelection();
if (!selection || !Array.isArray(selection.ids) || selection.ids.length !== 1) {
return null;
}
const objectType = String(selection.objectType || "").toLowerCase();
if (!objectType.includes("segment")) {
return null;
}
const segmentId = Number(selection.ids[0]);
if (!Number.isFinite(segmentId)) {
return null;
}
return wmeSDK.DataModel.Segments.getById({ segmentId });
}
function getVenueCategoryId(categoryType) {
if (categoryType === "canal") {
return getVenueCategoryIdByTargets(["canal"], "Canal");
}
return getVenueCategoryIdByTargets([
"river / stream",
"river/stream",
"river stream",
"river",
"stream"
], "River / Stream");
}
function getCategoryDisplayName(categoryType) {
return categoryType === "canal" ? "Canal" : "River / Stream";
}
function getVenueCategoryIdByTargets(normalizedTargets, displayName) {
const categories = wmeSDK.DataModel.Venues.getAllVenueCategories();
const match = categories.find((category) => {
const valuesToCheck = [
category.id,
category.name,
category.localizedName
].map(normalizeCategoryText);
return normalizedTargets.some((target) => {
const normalizedTarget = normalizeCategoryText(target);
return valuesToCheck.some((value) =>
value === normalizedTarget ||
value.includes(normalizedTarget)
);
});
});
if (!match) {
console.warn(
`${SCRIPT_NAME}: Could not find ${displayName} category. Available categories:`,
categories
);
throw new Error(`Could not find the ${displayName} venue category. See console for available categories.`);
}
return match.id;
}
function normalizeCategoryText(value) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/[_-]+/g, " ")
.replace(/\s*\/\s*/g, " / ")
.replace(/\s+/g, " ");
}
function formatSegmentLength(segment) {
const lengthMeters = Number(segment.length);
if (!Number.isFinite(lengthMeters)) {
return "Unknown";
}
const userSettings = wmeSDK.Settings.getUserSettings();
const isImperial = Boolean(userSettings && userSettings.isImperial);
if (isImperial) {
const feet = lengthMeters * 3.280839895;
if (feet >= 5280) {
return `${formatLengthNumber(feet / 5280)} mi`;
}
return `${formatLengthNumber(feet)} ft`;
}
if (lengthMeters >= 1000) {
return `${formatLengthNumber(lengthMeters / 1000)} km`;
}
return `${formatLengthNumber(lengthMeters)} m`;
}
function formatLengthNumber(value) {
if (value >= 100) return value.toFixed(0);
if (value >= 10) return value.toFixed(1);
return value.toFixed(2);
}
function getSegmentVertexCount(segment) {
if (
!segment ||
!segment.geometry ||
segment.geometry.type !== "LineString" ||
!Array.isArray(segment.geometry.coordinates)
) {
return 0;
}
return segment.geometry.coordinates.length;
}
function segmentToCenteredAreaPolygon(segment, widthMeters, options = {}) {
const miterLimit = options.miterLimit ?? 4.0;
const minSegmentLengthMeters = options.minSegmentLengthMeters ?? 0.05;
if (!segment || !segment.geometry || segment.geometry.type !== "LineString") {
throw new Error("Expected a WME Segment with GeoJSON LineString geometry.");
}
const coordinates = segment.geometry.coordinates;
if (!Array.isArray(coordinates) || coordinates.length < 2) {
throw new Error("Segment geometry must contain at least two coordinates.");
}
if (!Number.isFinite(widthMeters) || widthMeters <= 0) {
throw new Error("widthMeters must be a positive number.");
}
const halfWidth = widthMeters / 2;
const projection = createLocalProjection(coordinates);
const sourcePoints = coordinates.map(([lon, lat]) =>
projection.toXY(lon, lat)
);
const points = removeConsecutiveDuplicatePoints(
sourcePoints,
minSegmentLengthMeters
);
if (points.length < 2) {
throw new Error("Segment has no usable non-zero-length geometry.");
}
const offsetSegments = [];
for (let i = 0; i < points.length - 1; i++) {
const p0 = points[i];
const p1 = points[i + 1];
const dx = p1.x - p0.x;
const dy = p1.y - p0.y;
const len = Math.hypot(dx, dy);
if (len <= minSegmentLengthMeters) {
continue;
}
const ux = dx / len;
const uy = dy / len;
const leftNormal = { x: -uy, y: ux };
const rightNormal = { x: uy, y: -ux };
offsetSegments.push({
p0,
p1,
leftLine: {
a: addScaled(p0, leftNormal, halfWidth),
b: addScaled(p1, leftNormal, halfWidth)
},
rightLine: {
a: addScaled(p0, rightNormal, halfWidth),
b: addScaled(p1, rightNormal, halfWidth)
}
});
}
if (offsetSegments.length === 0) {
throw new Error("Segment has no usable non-zero-length segments.");
}
const leftOffsets = [];
const rightOffsets = [];
leftOffsets.push(offsetSegments[0].leftLine.a);
rightOffsets.push(offsetSegments[0].rightLine.a);
for (let i = 1; i < offsetSegments.length; i++) {
const previousSegment = offsetSegments[i - 1];
const nextSegment = offsetSegments[i];
const originalVertex = previousSegment.p1;
appendJoinPoint({
outputPoints: leftOffsets,
previousOffsetLine: previousSegment.leftLine,
nextOffsetLine: nextSegment.leftLine,
originalVertex,
halfWidth,
miterLimit
});
appendJoinPoint({
outputPoints: rightOffsets,
previousOffsetLine: previousSegment.rightLine,
nextOffsetLine: nextSegment.rightLine,
originalVertex,
halfWidth,
miterLimit
});
}
const lastSegment = offsetSegments[offsetSegments.length - 1];
leftOffsets.push(lastSegment.leftLine.b);
rightOffsets.push(lastSegment.rightLine.b);
const ringXY = leftOffsets.concat([...rightOffsets].reverse());
const cleanedRingXY = removeConsecutiveDuplicatePoints(
ringXY,
minSegmentLengthMeters
);
if (cleanedRingXY.length < 3) {
throw new Error("Generated polygon is invalid; fewer than three unique points.");
}
const ringLonLat = cleanedRingXY.map((point) => {
const lonLat = projection.toLonLat(point.x, point.y);
return [lonLat.lon, lonLat.lat];
});
closeGeoJsonRing(ringLonLat);
return {
type: "Polygon",
coordinates: [ringLonLat]
};
}
function appendJoinPoint({
outputPoints,
previousOffsetLine,
nextOffsetLine,
originalVertex,
halfWidth,
miterLimit
}) {
const intersection = intersectInfiniteLines(
previousOffsetLine.a,
previousOffsetLine.b,
nextOffsetLine.a,
nextOffsetLine.b
);
const maxMiterDistance = halfWidth * miterLimit;
if (
intersection &&
distance(intersection, originalVertex) <= maxMiterDistance
) {
outputPoints.push(intersection);
return;
}
outputPoints.push(previousOffsetLine.b);
outputPoints.push(nextOffsetLine.a);
}
function addScaled(point, vector, scale) {
return {
x: point.x + vector.x * scale,
y: point.y + vector.y * scale
};
}
function distance(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
function intersectInfiniteLines(a, b, c, d) {
const r = {
x: b.x - a.x,
y: b.y - a.y
};
const s = {
x: d.x - c.x,
y: d.y - c.y
};
const denominator = cross(r, s);
if (Math.abs(denominator) < 1e-12) {
return null;
}
const cMinusA = {
x: c.x - a.x,
y: c.y - a.y
};
const t = cross(cMinusA, s) / denominator;
return {
x: a.x + t * r.x,
y: a.y + t * r.y
};
}
function cross(a, b) {
return a.x * b.y - a.y * b.x;
}
function removeConsecutiveDuplicatePoints(points, toleranceMeters) {
const cleaned = [];
for (const point of points) {
if (
cleaned.length === 0 ||
distance(cleaned[cleaned.length - 1], point) > toleranceMeters
) {
cleaned.push(point);
}
}
return cleaned;
}
function closeGeoJsonRing(ring) {
if (ring.length === 0) return;
const first = ring[0];
const last = ring[ring.length - 1];
if (first[0] !== last[0] || first[1] !== last[1]) {
ring.push([first[0], first[1]]);
}
}
function createLocalProjection(lonLatCoordinates) {
const earthRadiusMeters = 6378137;
const averageLon =
lonLatCoordinates.reduce((sum, coordinate) => sum + coordinate[0], 0) /
lonLatCoordinates.length;
const averageLat =
lonLatCoordinates.reduce((sum, coordinate) => sum + coordinate[1], 0) /
lonLatCoordinates.length;
const lon0 = degreesToRadians(averageLon);
const lat0 = degreesToRadians(averageLat);
const cosLat0 = Math.cos(lat0);
return {
toXY(lon, lat) {
const lonRad = degreesToRadians(lon);
const latRad = degreesToRadians(lat);
return {
x: earthRadiusMeters * (lonRad - lon0) * cosLat0,
y: earthRadiusMeters * (latRad - lat0)
};
},
toLonLat(x, y) {
const lonRad = x / (earthRadiusMeters * cosLat0) + lon0;
const latRad = y / earthRadiusMeters + lat0;
return {
lon: radiansToDegrees(lonRad),
lat: radiansToDegrees(latRad)
};
}
};
}
function degreesToRadians(degrees) {
return degrees * Math.PI / 180;
}
function radiansToDegrees(radians) {
return radians * 180 / Math.PI;
}
})();
// ===== Update Indicator =====
(function () {
const META_URL = "https://update.greasyfork.org/scripts/583361.meta.js";
const SCRIPT_URL = "https://greasyfork.org/en/scripts/583361-wme-str-simplified";
const REMIND_KEY = "WMESTR_UpdateRemindUntil";
function getCurrentVersion() { try { if (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version) return GM_info.script.version; } catch (ignored) {} return ''; }
function versionToNumber(version) { return Number(String(version || '').replace(/\./g, '')); }
function isNewerVersion(latest, current) {
const latestNumber = versionToNumber(latest);
const currentNumber = versionToNumber(current);
return Number.isFinite(latestNumber) && Number.isFinite(currentNumber) && latestNumber > currentNumber;
}
function checkUpdate() {
try {
const remindUntil = localStorage.getItem(REMIND_KEY);
if (remindUntil && Date.now() < Number(remindUntil)) return;
if (typeof GM_xmlhttpRequest !== 'function') return;
GM_xmlhttpRequest({ method: 'GET', url: META_URL, onload: function (res) {
const match = res.responseText.match(/@version\s+(.+)/);
if (!match) return;
const latest = match[1].trim();
const current = getCurrentVersion();
if (!current) return;
if (isNewerVersion(latest, current)) showUI(latest);
}});
} catch (e) { console.warn('StR update check failed', e); }
}
function showUI(version) {
const root = document.querySelector("#wme-str-root");
if (!root) return;
let el = document.querySelector("#str-update-inline");
if (!el) {
el = document.createElement("div");
el.id = "str-update-inline";
el.style.margin = "0px 0 6px 0";
el.style.fontSize = "11px";
const versionEl = root.querySelector(".str-version");
if (versionEl && versionEl.parentNode) versionEl.insertAdjacentElement("afterend", el);
else root.prepend(el);
}
el.innerHTML = `
<div>
<span style="color:#0b5c20; font-weight:600;">Update available</span> (${version})
</div>
<div>
<a href="#" id="str-update-now">Update now</a>
|
<a href="#" id="str-update-later">Remind me later</a>
</div>
`;
el.querySelector("#str-update-now").onclick = function (e) {
e.preventDefault();
window.open(SCRIPT_URL, "_blank");
};
el.querySelector("#str-update-later").onclick = function (e) {
e.preventDefault();
const twoDays = 2 * 24 * 60 * 60 * 1000;
localStorage.setItem(REMIND_KEY, Date.now() + twoDays);
el.remove();
};
}
setTimeout(checkUpdate, 4000);
})();