Right-click functionality
// ==UserScript==
// @name WME - RightClick Functions
// @author GreekCaptain
// @version 1.0.2
// @description Right-click functionality
// @match https://www.waze.com/*/editor*
// @match https://www.waze.com/editor*
// @match https://beta.waze.com/*/editor*
// @match https://beta.waze.com/editor*
// @grant GM_setClipboard
// @grant unsafeWindow
// @grant GM_xmlhttpRequest
// @connect w-tools.org
// @require https://greasyfork.org/scripts/24851-wazewrap/code/WazeWrap.js
// @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADYAAAA2CAYAAACMRWrdAAAXzElEQVR42u2aebRcV3Xmf/ucO9Steq/qjZJlzZZlW7ZlbOMBY2PFjRtiY0hDIhIC2BDoAE7DomGRDlMbM6xOoKGTsBZ0CAsSktBgYRtjBwJm8iwPsuVJlmTNetLTm4d6NdzhnN1/VEkYYyN1eq3u/MFZq9Z7q96ruue7+9vf/va+B463Nm60/BtbG2/S4+5JjvN3A/j3/tX34/u+9cVLs6x1tgRhtVTpHVHbMyYi6sPAWBsJtnst5xCvXkKjgUpgA0nU+SVpu7HcFUXFWCtiLIgINsIEERLGmDCGsISJS5ggUglDgjDC2NAHUZiWyuWxoYHakxeuW3zPOy9bVwft7l30/xSYAfw5F73y2pnJsY9m7dZp3hcYY6f7hhd9ZOCUc28dPGfxwh033th8/gdVVd74gS+UxkfmbU+luWR+ZnrD1Nj4H2dpdqHYABsEqILaCBOWkO7LlquYuIIagwkibBBgghAbRkRRSByH9NbKI0uWLfrvX3rzJX+lnYsJ8qvgghejn3znO+6M9S/7H0cO7nl/q7mAiGQi4mwQThRZ0bz/li9Piohfs+7Csy583e9dHPUNnyxIy8/O7BKRe4BpMYYr3vif5incQVF9sCiyNeJ9j3c56tU4yTGRw0oHiOZtUAUx2CDEGYOJYmwWkDeFBirz09Gydqp/+Udfv/fl73nbpW+5/RO4G1V5PrjgBXNq0ya37txXfHh89MD7W82FPAhCCxKAt95rzWUZkVTWv+bPPv+ZYOmZVx1awBRZRhRaKict5bov/2jcHt77N1/71Ls+89Irz2ne9e2f9Trv+nzhEUmtjSKjxoqaEEl6kSDC5xmt1GP7e4lr/RTtlFLQJZUxWGsxKLEVP3voQCEmeOP//No9ra/eePnbbviEmhvhl4A9LwlvMGz7kr9kw+vPHh3ZfVOzUfelJLEKRlUVMN45nzmNr/jwn3/qsKxev/+hR/itpYF7/fpFbsOqXr+i5PTgVKO3vWTthss2vOryr37oo99beurKpfPTs69C3WprrQ0CI14sXgVchkvb+LDCOVdexhVvfBVX/+6lzLWhUYAJAoLung0qkRUKV9iFqclCguT8V7/jvVs++5IVOzbedJPdtmmTvkjEtgnAxPjBD7VbDRvHUR7YQGxgNM8ycS7XtNUub3jPf3nD9okyg/vvLL750bfZU09dY4MgJClFxAG8M2v5r9yxOf+ZrLz8TR/75D9+/+tfva2cBOcYayLUSZHmFN6gNkYlJqgN8LqP/wknvewcJvZPUR4sU0lCLn7DK0knJnnqzgeRtIk6p40iRxWMIDOjh7Rksw+KcMeZTz/9ohET2OY3Xn9Dz96nH/tiu9WoBNZKaBFVwYM05uZYc+kGY9Zdrc17/1m//tn3md6Bk1BUrDGkaUruHKUkkZeuW21HdjzjR6KhNcXhPRdMHj7Qp94bV2QIXiSuEFSq0m5mcvm73yH9V1wqt3767+XJm/9Fnnpolzz92LMiQ8vYcNV6Obh9hPnxaVAvoBj1lEIxWXNBcG7xGz/0ya99+vo312+44QZz11136VHl67LwBgEYeeqxtc4Vi0SMekScR5wrxLkCgOWXXcXOe+4373zDpYaolyJP2b1rNzt27pQwDKiUIr5502389O7NvPu1l5i8Pq/LX3LBsBHRcqVMFEYYa7XIc8maDSqDA5Qv2sA9X7+TuSeeICzFzEzM0psE7H/gYZ7cekjLAzVKSYkwDKiWI4mtSrvRpGg1vBRpcnj7M6cCbNt2ljxX0juasa1Dw0ZrbjFAGAY+jmJsEKnzaJHlWh4YII8H6c2mOP2M03RmZpZqby/fvukW/ft/+JaWSiW8Ko88+gTbtj9LqVxhTTUg7Rnwtf5BqyogYIOYUinWQLx6Y/XZH96pU48+TLnao+ILDYyiKK35ORmMckqRJRSP0ULr83VN2y3tKRki63zWrNOqz9Y6IH6NKqbtPDLGYG2AV/De4zF4RcJyj+YOaiWjNghR7xmfmOR9730PtWoPU9OzVHt7+NQn/ozxiUnwjoFySCt3gjEgBmMCMJa08ERhhKhy+hWX0NPfq5v/7mZ6B/uRAG145ZyXr+O8l65iy52PMD87SyiOwZ4Ilzvqc7NYTSnbmKxV7wRo0y8X4V8ubEGgXpUsL7RwiooFYxEb4ApPHAnYgDzPyYsC5xx/8bm/lHvvf5C8cOzes48/uPZ6fnb3ZjCWiZk6uSuwUUK1vx8TdDQuiSPCuCS+ucAzP7ybZa+4lDUXreftf/omLrn6Uv7Dda/m+g9cw913bWdq3wgrhhJZvbiH+uwsrZlRVtRyytYxNTFJVp/nuAVaQYyx2DDG2o4k2zDEYzVt5rTnJyEKcYWj2Uqp9Va45pqr9ds33czNt9xGs5Xy26/6d1zzmt/m0Mgojzw7gjed/80VqgNDlEoxDsvUzLwqytiPb2d6714WrVyCOe9Mzlt/JnmzwTf+1wNMbnma0wcNkxOzemR6klWDjt6hkANjOfsOjpMkJQZqPccH5hwaGoe3ioYJoRGC/iU4DLPjRxjZsoWNGy6mmXkS46k3WixfsYIb/uuHmZudZaC/j76+GmFo+eu/u4m99YLF2RgnLR6GIGB+vk57vkEpjhis9dAuPA4l3b6VnU89ycx0i6xwFJNTLO8vsSRSDu4bY7DcYv26iPGZkCf21Gk3G4jPEY1Is6y7+02/xnlYcITUkpw4Uea0hsegeYEOrmDV8hVc8bLzmW6kjI0dYXLScPrpp+ExVPuHcKIcPDTKrbfexpdu28w5r7ua9kOPE1tL0W5RK0c4rziFMLD01yqYKKFdQOqgvXMrQ/1VVq1azNR0nfbcPBeuUfKixBN7C6Zmm/i8jfqia79e0AP/KrAAwAQ0tI++JGRJr2P3XE4rrFJJquyihy/eciel5gwP338/SRyyavUqli9bShBYFhoLHNx/kIcee0ZW/c5bdX7PdhrTc1Rii6gnLMf0lEuIsRhjUOeIxJGUYxYtGmJgoI/R8VkmjoyyuJZRGwrZezhj+755gkCpJDGNAorC0RVZoDg+MGyASEcFp/MqFAHnrraM+hp7zCo0qrFz6TDr2qMkfTtwcxPc+/gudOe4mKSiOjdBXzEj/atP0+pAmamfb6Pa04PVgjCIJAgCdc4TBQGgRFFAnhcYExBbIW81mJ2e5PSThXZqeWpXSp6nLBtK8F4pxREnD/QyMzvH5MxsJ2rOHR+YFVExBnVKlmVkwWIO5T30rzyJM899JSNTOUVzHrN6LeteV+Mn/7KZak9FVBURxS5dS3N6gtWXXMDUlp/IzN5t5LVejIARg7FGOrYTjOkYH2MNRoQtmz3qPUFg9XEx4jEkcYCIYIxVEcF5T7VaZfikk0nLJaJSgjVGji8ehYqxIeVyQt/QIsltyESyjMGLr2H+8DiNnXuo79vHyEVncvq6Uznr1TV9+oHHSaSNT1uSZQX9p52FaU8z9tBdJJUSc9MToHD0+mEYYoyh0WySJAl5lhMEAUEQEIYhQTkRVWg2FljQzg1BRETA2IDx0UN4IO4ZwDlPXApPoB8T0VJSJiglLDRT5kt9euUf/g7P7Jnk6S3PEKbzEmmbQw89Ts+SxbpqzQBT9TOY2H+IKGmqz1IZWlpj37f+UdrNOd73/ut1fq7Orl27eMub38Sff/bzct1b36ztdotFixZz/bvfJZdfcSWfuOGjesONn5EP/uf36p69+0BVvvq1bzA1NU0QhsfcrIjBWkNzYYG40kez2SKKo+NHzFpLu52SLijJ4BKVnkEaYcJlV5zFeetXsu3xZ3XvyDT1w+Ns2z3PxJn9nHnBan1EDbM7d7L2rDWM/uw72jy8V0yc8NSTT3PttW9h9aqVnHbaWt5+3Vv18OhhwiDkmtdcxX3XXqevufoqarUa5XKZarXG1OQkBw8d0rHxcSnFMd57RDpFFgGvivOeoiiwxlIU7gXb/+fVsRRUEQGXtSVvLUi2/4CUxPDTO+6Tl5+zVF567iq54t+fL2943YVig0Se2nFEXrJuUE45/0zJdj9KY2JCepavppyUuPvee+W0tWsZGh6Wb/zDP8n1179Lbrn1e7Jo8SL55Gf+m7zjj94m9z/woGx5bKvUajWCMJA0y6S3tyrVai/OuQ6ozsih8/KdXCyXShLH0YnJvbUBUQBpUeDylLg5zlM//AlLFlflnJeuZsvW3axdMaB5YOTBf76bC37/lfzwscd5dt8O+iuOZzbfz5KXnEc2Pcb8nu04Y/nIRz4mR0YPc2RsnLm5OcZGR7n9u9/j4MgIt9/xA9JWiwu3ncu+Awf5269+nbA7GHJpStZuo1GEqhKEEaq+41/V470SBgHuRIA5VxBFIX39fdgwpulgYnQvN3/2SyxbvZix0Ul2RCW5/A9ey2lnLydv5vT2V5k/cpDZ7dvpW7mK+b07iDRl5UWX0pqZ4p4HH2F49SkE1X5u/u4dDC9fwUjqKS07hfFHH6Q6NMxIDq7cw8233EZYqWCsZeW6swjHj+CyjDAuMXfkcCffVFGvNBoNxIYkSXJ8YCKijWaTVppy0uLF9CUxUaBMzkwx+cQIWX2GBbXc/rm99C1bih9YQd5q0d7+IDSnqbfrpLNTXPD717LkjHU6O3JAkmqVucMHiHtqnFWuUKQpjckxyrU+0vocZ1x5FfsfvI9TLr4MG4ScdPo6lq4/Xx+9+Zuy4sKXc8olr+Dnf/0XHQo+h5JGBFXP0V7x1wIrCkjiEBHD5OQkQRTjPGT1eaLAstCaBTVo3mJy6iAmeBTBo3kT8haCI4gM4zufZmHisGiekacptSVL8X6WA489hA0jGtMTnSiMjbL/kQdJ+gaYOXSA4TWnMTc2RmP2p9K7eAnT+3fTmJokTBK8d3jncM7hvSPL2mACojg4AUsVdOxKFHWKZqvZAAxF2qJoe3ApmueYokVoBM0VQVDvOrKlShhFjG3bincFQRThC8foU4/hiwJjLUJnSHNU5Q499iBBKUGLAjEW1OOdI0zKaJHjioIgLhHGcbf96BqpPMdpQSmJTyTHvIoIqkoYhsSlEoXzLMzP4l0O3iOquKyF954winHeYYztFGDpUCWIIkRiVBUbdOpQEIZ47zHWdpW3s0Hp5o0Jw473E0sQxSiKDSNsHHeVUI99v3rf+d37F3JUL6CKQKPdJgxDKmFEmuU450nKZdqNebx3tNtNBhctpq+/nwN791Aul2k2m2RpBgKBDfDqcc4TBB2z26lFhnI5odVsElgLIlhrO2NxEcQLIIgIXro/cSDS8YR0alhR5Hh/lJIChTsREwzlchlFaDZbhFFMuZxQtGYw5HjnGBwa5oJLLtUsy2R6YoyLLroQVzgdHBoQVzh27NjJihXLGR4e0pmZWTHG4FxBtVoliiKt1xekr69PN33nVsmyrAPumHi98BReRCiKgkUnLWFyYoyiKDqUNlCciLtP04IoDKhWe2m2cyw5UTZOLZhnwXeGLCowOzMjO7c9RX2+zrannyGKI6nX64gIjWaT0dFR9u3bL81WiyzNiOOYSqVMo9kU9UoUR+LVdwuvB6STctpJPDGCYAH/ixmvCJOTk7TbbZJKlSxLUYSkVDk+sDgOUBVGj4xxck+hqweVfZMF+8adiBHCMGRhdobNP/4+VkLiWpUDBw4gIuzatRsUwjBgd1Eg3bsdBLbzEOJoXnXDYo3BWNsZ8HTgYI3pBEkBl4ENurKuiBGKtIl05d4XhZowwuFORO6NSFHnrKE2pRB5bL9nLg1QBCOCAVxQZt07/qPmfpwDN31P1q5dy/z8PO12myAM8M6TJAntVpOBwSHqC3XyLMf7zvsiQp7n9Pf3k+c5MzOzhFGE9548zxFjEISB1/6xDp+yjvmFBgp4sfj5Sca/+2VR78hdQWDsL1H5RYGtHIaBwHBw3PPwXqdRXCIMkJZ3WBvgVOkJ0c++eT23t+p889bv6+mnn8bQ8FAnoQvHzp07WbVqFaOjR6jVqgwODGCsQUToq9VoNJts3foE559/LmmaUe3txXnHjh3Pcs99DxAHoUi1H3v2bzG8qMYy8QwlEVlRsHk8xdz9Xfz0IaiAEU6s0XSIbtnTZGIup7eSiDHgFYwxXZob8jyVP/2Tj5HlIXnu+NGPfkSlUiFtpzjvQOGJJ56kKNyxPku1E62enh7GjhxBEbZufRxjLf19NbIsJ00zwiBARXDtNtObfyCPnryGkhaqaVsyG6tbmJJ84hBRFFJkbRHkxKi4/0ghOTGVsmKMEFhL/pw7IkDulGd2zwOe3t4KixYvOSbBabtNFJeIokjTtC3QyY8sy4iiiDT3DAwvprdaZaFeJ88yxEaUexPKPdBoNMiyDJEQt/V+6tufIu/tlY+//TX6/Xu2sPmBR8DTeSqBEkfBiVExjgN1maJe1eE6eqS+YzxVu82eUC6HoOC79qZUSojiiFarRbvZechZ6enVo2OAXjFUenppNOYplZIOrYuCMAxJkjKFK8iznDCM8EonBlGJ8vBSZhdaLF25mu+98y366tdfx/0PzWPEYaXQ0GRwIgXaeO+dK0A9SVzCGEuj1TqmZOp9Z7rkFe8cNgg4uH9v13mYY8p31LAe1eog6LiOo585ZmSN7ch9Z65BEISIDQkCC2kDac8R2BJ3/vRetS5jcGgYJxbVgrRdMNDnCU3hXxTY0VGjV2kZI1gjUjhHaGxHlkW6VlBJ07YkSZm+gQGdnpyQcqVCu9UmTduAYIOgE0nVjsMAmq0mcRRjw4Ai73jGIAi6TsN267Dg1Ssux7UWRBzqxw4S9AzxnTt+LLf/4MesWTYMjSmhVtF25mRkrMnyMs0Xj9imMxUgTqKxVgOfe2+zLNM8zzFisEbEq6rzXpJymfMuuFhLScLDD9yr688+G+c9fbUazhXs37+f1atPIUlKzM/PUy6XybKUxYsWMz8/T6WnwrPP7uahhx+RcpLgve+EWQS0MwQQ4xR1uKItzB7ChT20SxW9/+EnJAo67Yo1YqygQRiOAWzqYngeFW9UgFBr+1XGDgPLRFWNkaN1VlUV75xWajWMMTzx2BaajQZ79u4ljiLStI01hizLGRsfo9Vs0mh0861S4eDBkY5NCwNarY4f1aOWQo/RVsF30jprQp4qYkTadWXBSCIFWd5W1ZIiYsCMLVt7zj7uvPUYhl85DrFx40a7adMmt/aM87+RthpvUdXCWhs4r6TtNl49adoizzIBxXtPFMfabrelM7d0GBHCKOrYHYUgCPDe84tZokE61NYwDKUz0+iWEmM6Lr/rSEREEYOI6Rx5UBVQdUUulZ5qDhJUemu37n7m4d+FjRY2uRcUj03dRItK8Rddnr01z1IpnOv6uF+0DF69vurq17Jn10727t7FWWefrXOzsxhjyPNC2u2WVqvLAMW5jmI6V6AKpVLM7NwcgQ1wrtBKpUcKV6gRQ57nBIGl0WhSX1joHuEAlQ5BtaM4eO80zzPKlV7pKVe+0AnKL/b/Aqq4ycFG+/TWTQ+fcur6z4sxHyzyPDPGBurVeI4qokdENEkqIiLEccyq1asZHh7Woij0mW3bZO3aUzUMQ3qrVQ4eOEhvbw9Z3mkaFVi5YgUiQpqlao3tWKyBfur1OgD33H2fWGtRURDfaWY76uVdURTlck8Ul5LPPb7l5/dBh2nHO5kjsNHAJrdy1Zl/61zxzsIVeO+LLG2LsUaKoiBL02O00q7rN8YQBJaicJLnuQaBlVIp0Xq9LkaMHu1AgiDAuc7Ncd1erNuaiDGGOI41L3IxHYpq1zh3xx0aJOUeagPDf7Pzyc3vht+zsMnzvHMe8mvOWAngV64++wNFkX4c6EvbbYoi/0UX2z1FQ4eenQ94PTaPFwSveswZdGqcAbT74MMfayDRo1Tv1Eo5auGO+v5uUxpG8UxPte/Tu7dv+UJ3LqrPB3W8s1THwK0564LleaP5h17dla5wq1yRlwrvjKqCYrontUQAr17oektjrDxn2ol2AdHtjtX7Y+8fFZfngun+cKguGGv3hHH8k0rU90+7dj00cvSs17/29NvRacEx/m7YsCFI00Vhnrckz1Mpikycy6U7kxQGuoV+0p3Id0P/r39rIY79hrPPbn/lK1/Jn6Pfv6SA/zfLsGFD8EIj8f+Hy8CJ70H+FReQ/0/AlN+s36zfrH8z638DNlpxm6DohBEAAAAASUVORK5CYII=
// @run-at document-start
// @license MIT
// @namespace https://greasyfork.org/users/1561762
// ==/UserScript==
(() => {
"use strict";
const UW = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
const SCRIPT_ID = "wme-rightclick-functions";
const SCRIPT_NAME = "WME - RightClick Functions";
const SCRIPT_VERSION = "1.0.2";
const CHANGELOG_SEEN_KEY = `${SCRIPT_ID}:changelogSeenVersion:v2`;
const CHANGELOG_ITEMS = [
"New: Visual and accesibility changes",
];
const EDITOR_BASE = `${location.origin}${location.pathname}`;
const GMAPS_BASE = "https://www.google.com/maps?q=";
if (UW.__WME_RIGHTCLICK_FUNCTIONS__) return;
UW.__WME_RIGHTCLICK_FUNCTIONS__ = true;
let sdk = null;
let enabled = true;
let lastLonLat = null;
let cachedUserLevel = null;
let rankIsZeroBased = null;
let lockIsZeroBased = null;
let attrClip = null;
const PIN_KEY = `${SCRIPT_ID}:pinnedPlaces:v1`;
const PIN_POS_KEY = `${SCRIPT_ID}:pinnedPlacesPos:v1`;
const PIN_PANEL_SIZE_KEY = `${SCRIPT_ID}:pinsPanelSize:v1`;
const PIN_PANEL_COLLAPSED_KEY = `${SCRIPT_ID}:pinsPanelCollapsed:v1`;
const PIN_PANEL_ALWAYS_VISIBLE_EMPTY_KEY = `${SCRIPT_ID}:pinsPanelAlwaysVisibleEmpty:v1`;
const PIN_BUBBLE_POS_KEY = `${SCRIPT_ID}:pinsBubblePos:v1`;
const PIN_MINIMIZE_MODE_KEY = `${SCRIPT_ID}:pinsMinimizeMode:v1`;
const PIN_PANEL_MINIMIZED_KEY = `${SCRIPT_ID}:pinsPanelMinimized:v1`;
// --- Pin name validation ---
const PIN_NAME_MAX = 28;
function validatePinName(name, fallback) {
const s = String(name ?? "").trim();
const val = s || String(fallback ?? "").trim() || "Pinned place";
if (!val) return { ok: false, value: "", len: 0, msg: "Pin name can't be empty." };
if (val.length > PIN_NAME_MAX) {
return { ok: false, value: val, len: val.length, msg: `Pin name is ${val.length} chars. Max is ${PIN_NAME_MAX}. Rename it.` };
}
return { ok: true, value: val, len: val.length, msg: "" };
}
let _pinsMem = null;
let _pinsSaveTimer = null;
let _pinsLastJson = null;
const _DEBOUNCE_SAVE_MS = 250;
// ---------- Lightweight scheduler (performance) ----------
// Replaces some hot setInterval loops with an adaptive timeout loop
// that slows down when the tab is hidden and can be gated by an `active()` predicate.
function _createAdaptiveLoop(fn, baseMs, opts = {}) {
let stopped = false;
let t = null;
const active = typeof opts.active === "function" ? opts.active : () => true;
const immediateOnVisible = opts.immediateOnVisible !== false;
function delay() {
// Slow down when tab is hidden to cut CPU usage.
if (document.hidden) return Math.max(baseMs * 4, 1200);
return baseMs;
}
function tick() {
if (stopped) return;
try {
if (active()) fn();
} catch {}
t = setTimeout(tick, delay());
}
// Kick off
t = setTimeout(tick, baseMs);
// Optional: run once when coming back to foreground
function onVis() {
if (stopped) return;
if (!document.hidden && immediateOnVisible) {
try { if (active()) fn(); } catch {}
}
}
try { document.addEventListener("visibilitychange", onVis, { passive: true }); } catch {}
return () => {
stopped = true;
try { if (t) clearTimeout(t); } catch {}
try { document.removeEventListener("visibilitychange", onVis); } catch {}
};
}
function _schedulePinsSaveNow(pins) {
try {
if (_pinsSaveTimer) clearTimeout(_pinsSaveTimer);
} catch {}
_pinsSaveTimer = setTimeout(() => {
_pinsSaveTimer = null;
try {
const json = JSON.stringify(pins || []);
if (json !== _pinsLastJson) {
localStorage.setItem(PIN_KEY, json);
_pinsLastJson = json;
}
} catch {}
}, _DEBOUNCE_SAVE_MS);
}
function _flushPinsSave() {
try {
if (_pinsSaveTimer) {
clearTimeout(_pinsSaveTimer);
_pinsSaveTimer = null;
}
} catch {}
try {
const pins = _pinsMem;
if (!pins) return;
const json = JSON.stringify(pins || []);
if (json !== _pinsLastJson) {
localStorage.setItem(PIN_KEY, json);
_pinsLastJson = json;
}
} catch {}
}
let pinsNoClusterUntil = 0;
const PIN_COLOR_PRESETS = ["#ff8a00","#ff3b30","#007aff","#34c759","#ffd60a","#af52de"];
const PIN_CUSTOM_COLORS_KEY = `${SCRIPT_ID}:pinCustomColors:v1`;
function loadCustomPinColors() {
try {
const raw = localStorage.getItem(PIN_CUSTOM_COLORS_KEY);
const arr = JSON.parse(raw || "[]");
const out = Array.isArray(arr) ? arr : [];
return out
.map(c => normalizePinColor(c))
.filter(Boolean)
.slice(0, 4);
} catch { return []; }
}
function saveCustomPinColors(colors) {
try {
const arr = Array.isArray(colors) ? colors : [];
const norm = arr.map(c => normalizePinColor(c)).filter(Boolean).slice(0, 4);
localStorage.setItem(PIN_CUSTOM_COLORS_KEY, JSON.stringify(norm));
} catch {}
}
function pickRandomPinColor() {
try { return PIN_COLOR_PRESETS[Math.floor(Math.random() * PIN_COLOR_PRESETS.length)] || "#ff8a00"; }
catch { return "#ff8a00"; }
}
const PIN_GROUPS_KEY = `${SCRIPT_ID}:pinGroups:v1`;
const PIN_GROUP_FILTER_KEY = `${SCRIPT_ID}:pinGroupFilter:v1`;
const PIN_LAST_GROUP_KEY = `${SCRIPT_ID}:pinLastGroup:v1`;
const PIN_GROUP_UI_KEY = `${SCRIPT_ID}:pinGroupUi:v1`;
function loadPinGroupUi() {
try {
if (loadPinGroupUi._cache) return loadPinGroupUi._cache;
const raw = localStorage.getItem(PIN_GROUP_UI_KEY);
const obj = JSON.parse(raw || "{}");
const out = (obj && typeof obj === "object") ? obj : {};
loadPinGroupUi._cache = out;
return out;
} catch {
loadPinGroupUi._cache = {};
return {};
}
}
loadPinGroupUi._cache = null;
function savePinGroupUi(ui) {
try {
const out = (ui && typeof ui === "object") ? ui : {};
loadPinGroupUi._cache = out;
// Debounced write to avoid hammering localStorage on rapid UI toggles.
if (savePinGroupUi._t) clearTimeout(savePinGroupUi._t);
savePinGroupUi._t = setTimeout(() => {
try { localStorage.setItem(PIN_GROUP_UI_KEY, JSON.stringify(loadPinGroupUi._cache || {})); } catch {}
savePinGroupUi._t = null;
}, 120);
} catch {}
}
savePinGroupUi._t = null;
function isGroupCollapsed(gid) {
try {
const ui = loadPinGroupUi();
return !!(ui && ui[String(gid)] && ui[String(gid)].collapsed);
} catch { return false; }
}
function setGroupCollapsed(gid, collapsed) {
try {
const id = String(gid);
const ui = loadPinGroupUi();
ui[id] = { ...(ui[id] || {}), collapsed: !!collapsed };
savePinGroupUi(ui);
} catch {}
}
function _uid() {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
}
function loadPinGroups() {
try {
const raw = localStorage.getItem(PIN_GROUPS_KEY);
const arr = JSON.parse(raw || "[]");
const groups = Array.isArray(arr) ? arr : [];
if (!groups.some(g => g && g.id === "default")) {
groups.unshift({ id: "default", name: "(no folder)", emoji: "" });
}
try {
const def = groups.find(g => g && g.id === "default");
if (def && (String(def.name || "").trim() === "Default" || !String(def.name || "").trim())) def.name = "(no folder)";
} catch {}
return groups
.filter(g => g && typeof g.id === "string" && typeof g.name === "string" && g.id.trim())
.map(g => ({ id: String(g.id), name: String(g.name).trim() || "Group", emoji: (typeof g.emoji === "string") ? String(g.emoji) : "" }));
} catch {
return [{ id: "default", name: "(no folder)", emoji: "" }];
}
}
function savePinGroups(groups) {
try { localStorage.setItem(PIN_GROUPS_KEY, JSON.stringify(groups || [])); } catch {}
}
function getGroupName(id) {
const gid = String(id || "default");
const groups = loadPinGroups();
const g = groups.find(x => x && x.id === gid);
return g ? g.name : (gid === "default" ? "(no folder)" : "Group");
}
function setDefaultGroupName(name) {
const nm = String(name || "").trim() || "(no folder)";
const groups = loadPinGroups();
const idx = groups.findIndex(g => g && g.id === "default");
if (idx >= 0) groups[idx] = { ...groups[idx], name: nm };
else groups.unshift({ id: "default", name: nm, emoji: "" });
savePinGroups(groups);
}
function normalizeGroupId(id) {
const gid = String(id || "default") || "default";
const groups = loadPinGroups();
if (groups.some(g => g && g.id === gid)) return gid;
return "default";
}
function getCurrentGroupFilter() {
try {
const v = localStorage.getItem(PIN_GROUP_FILTER_KEY);
if (!v || v === "all") return "all";
return normalizeGroupId(v);
} catch {
return "all";
}
}
function setCurrentGroupFilter(id) {
try {
const v = (id == null || id === "all") ? "all" : normalizeGroupId(id);
localStorage.setItem(PIN_GROUP_FILTER_KEY, v);
} catch {}
}
function getLastGroup() {
try {
const v = localStorage.getItem(PIN_LAST_GROUP_KEY);
if (!v) return "default";
return normalizeGroupId(v);
} catch { return "default"; }
}
function setLastGroup(id) {
try { localStorage.setItem(PIN_LAST_GROUP_KEY, normalizeGroupId(id)); } catch {}
}
function createPinGroup(name, emoji) {
const nm = String(name || "").trim();
if (!nm) return "default";
const groups = loadPinGroups();
const id = _uid();
groups.push({ id, name: nm, emoji: (typeof emoji === 'string') ? emoji : '' });
savePinGroups(groups);
return id;
}
function getNextPinNumber(pins) {
let max = 0;
try {
for (const p of (pins || [])) {
const name = String(p && p.name ? p.name : "");
const m = /^Pin\s*#\s*(\d+)/i.exec(name);
if (m) {
const n = Number(m[1]) || 0;
if (n > max) max = n;
}
}
} catch {}
return max + 1;
}
const PINS_LAYER_KEY = `${SCRIPT_ID}:pinsLayerVisible:v1`;
function getPinsLayerVisible() {
try {
const v = localStorage.getItem(PINS_LAYER_KEY);
if (v == null) return true;
return v === "1" || v === "true";
} catch { return true; }
}
function setPinsLayerVisible(v) {
try { localStorage.setItem(PINS_LAYER_KEY, v ? "1" : "0"); } catch {}
try {
if (pinsOlLayer && typeof pinsOlLayer.setVisibility === "function") {
const cur = (typeof pinsOlLayer.getVisibility === "function") ? pinsOlLayer.getVisibility() : null;
if (cur !== !!v) pinsOlLayer.setVisibility(!!v);
}
} catch {}
try { renderPinsMarkers(); } catch {}
}
const PINS_NAMES_KEY = `${SCRIPT_ID}:pinsShowNamesOnMap:v1`;
function getPinsShowNamesOnMap() {
try {
const v = localStorage.getItem(PINS_NAMES_KEY);
if (v == null) return true; // default ON
return v === "1" || v === "true";
} catch { return true; }
}
function setPinsShowNamesOnMap(v) {
try { localStorage.setItem(PINS_NAMES_KEY, v ? "1" : "0"); } catch {}
try { renderPinsMarkers(); } catch {}
}
const PINS_NAMES_MINZOOM_KEY = `${SCRIPT_ID}:pinsNamesMinZoom:v1`;
function getPinsNamesMinZoom(){
try{
const v = localStorage.getItem(PINS_NAMES_MINZOOM_KEY);
if (v == null) return 9; // default
const n = Number(v);
if (!Number.isFinite(n)) return 9;
return Math.min(22, Math.max(1, Math.round(n)));
}catch{ return 9; }
}
function setPinsNamesMinZoom(z){
try{
const n = Math.min(22, Math.max(1, Math.round(Number(z) || 9)));
localStorage.setItem(PINS_NAMES_MINZOOM_KEY, String(n));
}catch{}
try{ renderPinsMarkers(); }catch{}
}
let pinsDrag = { active:false, startX:0, startY:0, baseLeft:12, baseTop:12, pointerId:null };
let pinsPanelEl = null;
// Pins bubble minimize state (strict-mode safe declarations)
let pinsBubbleEl = null;
let pinsBubbleDrag = {
active: false,
pointerId: null,
startX: 0,
startY: 0,
baseLeft: 0,
baseTop: 0,
moved: false,
movedAt: 0,
};
let _pinsBubbleHideT = 0;
let _pinsPanelMinimizeHideT = 0;
let _pinsBubbleForceHideUntil = 0;
let _pinsPanelSuppressDockUntil = 0;
let _pinsPanelSuppressROUntil = 0;
let _pinsPanelLastAutoAt = 0;
const _pinsNow = () => {
try { return (typeof performance !== "undefined" && performance.now) ? performance.now() : Date.now(); }
catch { return Date.now(); }
};
let pinsCache = [];
let reminderIntervalId = null;
const firedReminderKeys = new Set();
const activeReminderNotices = new Set();
let bellLoopId = null;
let missedRemindersChecked = false;
const reminderTimers = new Map(); // pinId -> timeoutId
function clearReminderTimer(pinId){
try{
const t = reminderTimers.get(String(pinId));
if (t) { clearTimeout(t); }
reminderTimers.delete(String(pinId));
}catch{}
}
function scheduleReminderTimer(pin){
try{
if (!pin || !pin.id) return;
clearReminderTimer(pin.id);
if (!pin.reminderAt || pin.reminderDone) return;
const at = Number(pin.reminderAt);
if (!Number.isFinite(at)) return;
const delay = Math.max(0, at - Date.now());
const d = Math.min(delay, 0x7fffffff);
const tid = setTimeout(() => { try { triggerReminderById(String(pin.id), at); } catch {} }, d);
reminderTimers.set(String(pin.id), tid);
}catch{}
}
function scheduleAllReminderTimers(){
try{
const pins = loadPins();
for (const p of pins) scheduleReminderTimer(p);
}catch{}
}
function triggerReminderById(pinId, expectedAt){
try{
const pins = loadPins();
const p = pins.find(x => x && x.id === String(pinId));
if (!p) return;
const atRaw = p.reminderAt;
if (atRaw == null) return;
const at = Number(atRaw);
if (!Number.isFinite(at) || at <= 0) return;
if (expectedAt != null && Number(expectedAt) !== at) return;
if (p.reminderDone) return;
if (at > Date.now()) { scheduleReminderTimer(p); return; }
let shown = false;
try { shown = showReminderNotice([p]) === true; } catch(e){ try { console.error("[WME Pins] showReminderNotice failed", e); } catch{} }
if (!shown) {
try { toast(`Reminder: ${p.name || "Pinned place"}`); } catch {}
}
try { sendExternalReminderWebhook(p); } catch {}
p.reminderDone = true;
p.reminderFiredAt = Date.now();
savePins(pins);
renderPinsPanel();
try { applyPinsPanelMinimizedOnLoad(); } catch {}
clearReminderTimer(pinId);
}catch(e){
try { console.error("[WME Pins] triggerReminderById failed", e); } catch{}
}
}
function clearFiredKeysForPin(pinId){
try{
const pref=String(pinId)+":";
for (const k of Array.from(firedReminderKeys)) {
if (String(k).startsWith(pref)) firedReminderKeys.delete(k);
}
}catch{}
}
let pinsMapSyncStarted = false;
let lastBellAt = 0;
let pinsCountdownTimerId = null;
let _pinsCountdownTicking = false;
let _pinCountdownEls = new Map();
let _pinCountdownModes = new Map(); // pinId -> 0/1/2
let _pinCountdownTipModes = new Map(); // pinId -> 0/1/2 (tooltip only)
let _pinRowEls = new Map();
let pinsMarkersEl = null;
let pinsOlLayer = null;
const pinsOlMarkers = new Map(); // pinId -> OpenLayers.Marker
const pinsOlClusterMarkers = new Map(); // clusterId -> OpenLayers.Marker
const pinMarkerEls = new Map();
const pinClusterEls = new Map();
let pinsMarkersIntervalId = null;
const pasteCfg = { speed: true, lock: true, direction: true, elevation: true };
let speedDirChoice = "BOTH";
const menuState = {
root: null,
sub: null,
sub2: null,
subCloseTimer: null,
outsideCloseHandler: null,
escHandler: null,
subContext: null,
sub2Context: null,
rootLL: null,
rootType: null,
subAnchorRow: null,
sub2AnchorRow: null,
};
const ICONS = {
copy: svg(`<path d="M8 7h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2zm-2 8H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v0H8a4 4 0 0 0-4 4v6z"/>`),
paste: svg(`<path d="M19 21H9a2 2 0 0 1-2-2V7h2v12h10v2z"/><path d="M16 3H10a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2zm0 14H10V5h6v12z"/>`),
street: svg(`<path d="M5 3h14v2H5V3zm0 16h14v2H5v-2zm1-7h2v2H6v-2zm4 0h2v2h-2v-2zm4 0h2v2h-2v-2zm4 0h2v2h-2v-2zM4 7h16v10H4V7z"/>`),
lock: svg(`<path d="M7 10V8a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-6a2 2 0 0 1 2-2h1zm2 0h6V8a3 3 0 0 0-6 0v2z"/>`),
speed: svg(`<path d="M12 4a9 9 0 1 0 9 9h-2a7 7 0 1 1-7-7V4zm6.5 6.5-5.3 3a2 2 0 1 1-1-1l3-5.3 3.3 3.3z"/>`),
zoom: svg(`<path d="M10 18a8 8 0 1 1 5.3-2l4.3 4.3-1.4 1.4L13.9 17.4A8 8 0 0 1 10 18zm0-2a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"/><path d="M9 9h2V7h2v2h2v2h-2v2h-2v-2H9V9z"/>`),
endpoints: svg(`<path d="M7 7a3 3 0 1 1 0 6 3 3 0 0 1 0-6zm10 4a3 3 0 1 1 0 6 3 3 0 0 1 0-6z"/><path d="M9.5 11.5h5v2h-5v-2z"/>`),
tools: svg(`<path d="M21 7.5 18.5 5l-2 2 2.5 2.5L21 7.5zM3 17.5V21h3.5l10-10-3.5-3.5-10 10z"/><path d="M14.2 6.8 17 9.6"/>`),
more: svg(`<path d="M5 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"/>`),
minus: svg(`<path d="M6 12h12v2H6z"/>`),
mapPin: svg(`<path d="M12 22s7-5.2 7-12a7 7 0 0 0-14 0c0 6.8 7 12 7 12zm0-10a2 2 0 1 1 0-4 2 2 0 0 1 0 4z"/>`),
folder: svg(`<path d="M10 4l2 2h8a2 2 0 0 1 2 2v3H2V6a2 2 0 0 1 2-2h6zm14 9v7a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-7h22z"/>`),
folderPlus: svg(`<path d="M10 4l2 2h8a2 2 0 0 1 2 2v3H2V6a2 2 0 0 1 2-2h6zm14 9v7a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-7h22z"/><path d="M18.5 14.5v6h-2v-6h2z"/><path d="M14.5 18.5h6v-2h-6v2z"/>`),
link: svg(`<path d="M10.6 13.4a1 1 0 0 0 1.4 1.4l3.6-3.6a3 3 0 0 0-4.2-4.2l-1.6 1.6a1 1 0 1 0 1.4 1.4l1.6-1.6a1 1 0 1 1 1.4 1.4l-3.6 3.6z"/><path d="M13.4 10.6a1 1 0 0 0-1.4-1.4l-3.6 3.6a3 3 0 0 0 4.2 4.2l1.6-1.6a1 1 0 1 0-1.4-1.4l-1.6 1.6a1 1 0 1 1-1.4-1.4l3.6-3.6z"/>`),
jump: svg(`<path d="M10 16a6 6 0 1 1 4.47-2.01l4.27 4.28-1.41 1.41-4.28-4.27A5.98 5.98 0 0 1 10 16zm0-2a4 4 0 1 0 0-8 4 4 0 0 0 0 8z"/>`),
refresh: svg(`<path d="M17.65 6.35A7.95 7.95 0 0 0 12 4V1L7 6l5 5V7c2.76 0 5 2.24 5 5a5 5 0 0 1-8.66 3.54l-1.42 1.42A6.98 6.98 0 0 0 19 12c0-2.21-.9-4.21-2.35-5.65z"/>`),
gear: svg(`<path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zm9 4-2 .7a7.9 7.9 0 0 1-.6 1.5l1.2 1.8-1.8 1.8-1.8-1.2a7.9 7.9 0 0 1-1.5.6L13 21h-2l-.7-2a7.9 7.9 0 0 1-1.5-.6l-1.8 1.2-1.8-1.8 1.2-1.8a7.9 7.9 0 0 1-.6-1.5L3 13v-2l2-.7a7.9 7.9 0 0 1 .6-1.5L4.4 7l1.8-1.8 1.8 1.2a7.9 7.9 0 0 1 1.5-.6L11 3h2l.7 2a7.9 7.9 0 0 1 1.5.6l1.8-1.2L20 7l-1.2 1.8c.25.48.46.98.6 1.5L21 11v2z"/>`),
properties: svg(`<path d="M4 7h10v2H4V7zm0 8h10v2H4v-2zm12-9h4v2h-4V6zm0 8h4v2h-4v-2z"/><path d="M14 6h2v6h-2V6zm-6 4h2v10H8V10zm10-3h2v10h-2V7z"/>`),
ext: svg(`<path d="M14 3h7v7h-2V6.4l-9.3 9.3-1.4-1.4L17.6 5H14V3z"/><path d="M5 5h6v2H7v10h10v-4h2v6H5V5z"/>`),
grip: svg(`<path d="M9 6h2v2H9V6zm4 0h2v2h-2V6zM9 10h2v2H9v-2zm4 0h2v2h-2v-2zM9 14h2v2H9v-2zm4 0h2v2h-2v-2z"/>`),
trash: svg(`<path d="M9 3h6l1 2h5v2H3V5h5l1-2zm1 6h2v10h-2V9zm4 0h2v10h-2V9zM7 9h2v10H7V9z"/>`),
bell: svg(`<path d="M12 22a2 2 0 0 0 2-2H10a2 2 0 0 0 2 2zm6-6V11a6 6 0 1 0-12 0v5L4 18v1h16v-1l-2-2z"/>`),
edit: svg(`<path d="M3 17.25V21h3.75L17.8 9.95l-3.75-3.75L3 17.25z"/><path d="M20.7 7.04a1 1 0 0 0 0-1.41l-2.34-2.34a1 1 0 0 0-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>`),
eye: svg(`<path d="M12 5c-7 0-10 7-10 7s3 7 10 7 10-7 10-7-3-7-10-7zm0 12a5 5 0 1 1 0-10 5 5 0 0 1 0 10z"/><path d="M12 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5z"/>`),
arrows: svg(`<path d="M7 7h6V5l4 3-4 3V9H7V7zm10 10h-6v2l-4-3 4-3v2h6v2z"/>`),
select: svg(`<path d="M3 3h6v2H5v4H3V3zm16 0h2v6h-2V5h-4V3h4zM3 15h2v4h4v2H3v-6zm16 4v-4h2v6h-6v-2h4z"/>`),
chain: svg(`<path d="M7 12a3 3 0 0 1 3-3h2v2h-2a1 1 0 0 0 0 2h2v2h-2a3 3 0 0 1-3-3zm7-3h2a3 3 0 0 1 0 6h-2v-2h2a1 1 0 0 0 0-2h-2V9z"/><path d="M10 11h4v2h-4v-2z"/>`),
};
if (!ICONS.chevDown) {
ICONS.chevDown = svg(`<path d="M7 10l5 5 5-5z"/>`);
}
function svg(pathD) {
return `<svg viewBox="0 0 24 24" aria-hidden="true">${pathD}</svg>`;
}
function withIcon(iconSvg, text, iconClass = "") {
const cls = iconClass ? `wmeRcI ${iconClass}` : "wmeRcI";
return `<div class="wmeRcLbl"><span class="${cls}">${iconSvg}</span><span class="wmeRcT">${text}</span></div>`;
}
function ensureCSS() {
if (document.getElementById("wmeRcCss")) return;
const s = document.createElement("style");
s.id = "wmeRcCss";
s.textContent = `
.wmeRcToast{position:fixed;right:16px;bottom:16px;z-index:2147483647;background:rgba(20,20,22,.90);color:#fff;border:1px solid rgba(255,255,255,.18);border-radius:12px;padding:10px 12px;box-shadow:0 10px 30px rgba(0,0,0,.35);backdrop-filter:blur(10px);font:13px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;opacity:0;transform:translate3d(0,10px,0);transition:.18s ease;max-width:min(560px,calc(100vw - 32px));pointer-events:none;will-change:transform,opacity;}
.wmeRcToast.show{opacity:1;transform:translate3d(0,0,0);}
/* Pin click toast (bottom-center) */
.wmeRcPinHitPill{position:fixed;left:50%;bottom:22px;top:auto;right:auto;z-index:2147483647;
display:flex;align-items:center;gap:10px;
max-width:min(520px,calc(100vw - 32px));
padding:10px 12px;
background:rgba(20,20,22,.72);color:#fff;border:1px solid rgba(255,255,255,.16);
border-radius:14px;box-shadow:0 18px 54px rgba(0,0,0,.45);
backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
font:13px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
opacity:0;transform:translate3d(-50%,14px,0);transition:.18s ease;pointer-events:auto;}
.wmeRcPinHitPill.show{opacity:1;transform:translate3d(-50%,0,0);}
.wmeRcPinHitText{font-weight:900;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0;}
.wmeRcPinHitClose{width:26px;height:26px;border-radius:10px;border:1px solid rgba(255,255,255,.14);
background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;cursor:pointer;flex:0 0 auto;transition:.16s ease;user-select:none;}
.wmeRcPinHitClose:hover{background:rgba(255,255,255,.10);}
/* Reminder notice stack: bottom-right, keep clear of WME right-side controls */
.wmeRcNoticeStack{position:fixed;right:96px;bottom:118px;z-index:2147483647;display:flex;flex-direction:column;gap:10px;align-items:flex-end;pointer-events:none;}
.wmeRcNotice{position:relative;min-width:260px;max-width:min(440px,calc(100vw - 32px));
background:rgba(16,16,18,.55);color:#fff;border:1px solid rgba(255,255,255,.14);border-radius:16px;padding:13px 13px 11px;
box-shadow:0 18px 54px rgba(0,0,0,.48);backdrop-filter:blur(22px) saturate(140%);-webkit-backdrop-filter:blur(22px) saturate(140%);
font:13px/1.25 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
opacity:0;transform:translate3d(0,10px,0);transition:.22s ease;pointer-events:auto;}
.wmeRcNotice.show{opacity:1;transform:translate3d(0,0,0);}
.wmeRcNoticeTitle{font-weight:900;font-size:12px;letter-spacing:.22px;text-transform:uppercase;margin-bottom:6px;opacity:.80;}
.wmeRcNoticeMsg{font-size:16px;font-weight:900;letter-spacing:.15px;opacity:.98;margin-bottom:6px;line-height:1.15;white-space:normal;word-break:break-word;}
.wmeRcNoticeNote{font-size:13px;opacity:.95;margin-top:2px;margin-bottom:10px;white-space:normal;word-break:break-word;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.12);border-radius:12px;padding:8px 10px;}
.wmeRcNoticeActions{display:flex;gap:8px;justify-content:flex-start;}
.wmeRcNoticeBtn{
appearance:none;
display:flex;align-items:center;justify-content:center;line-height:1;
border:1px solid rgba(255,255,255,.16);
background:rgba(255,255,255,.06);
color:#fff;
border-radius:12px;
padding:8px 12px;
font-weight:900;
cursor:pointer;
transition:transform .14s ease, background .14s ease, box-shadow .14s ease, border-color .14s ease;
box-shadow:inset 0 1px 0 rgba(255,255,255,.06);
}
.wmeRcNoticeBtn:hover{
background:rgba(255,255,255,.10);
transform:translate3d(0,-1px,0);
box-shadow:0 12px 28px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.08);
}
.wmeRcNoticeBtn:active{
background:rgba(255,255,255,.12);
transform:translate3d(0,0,0) scale(.99);
}
.wmeRcNoticeBtn.primary{
border-color:rgba(255,255,255,.18);
background:rgba(255,255,255,.10);
}
.wmeRcNoticeBtn.primary:hover{
background:rgba(255,255,255,.14);
transform:translate3d(0,-1px,0);
}
.wmeRcRemNoteWrap{margin-top:10px;background:rgba(255,255,255,.04);border:1px solid rgba(255,255,255,.10);border-radius:12px;padding:8px 10px;}
.wmeRcRemNoteLbl{font-weight:800;font-size:12px;opacity:.85;margin-bottom:6px;}
.wmeRcRemNoteInp{width:100%;height:92px;resize:none;outline:none;border:none;background:transparent;color:#fff;font:13px/1.35 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;opacity:.92;overflow:auto;scrollbar-width:none;}
.wmeRcRemNoteInp::-webkit-scrollbar{width:0;height:0;}
.wmeRcSnoozePop{
position:fixed;left:0;top:0;z-index:2147483647;
min-width:260px;max-width:320px;
padding:12px 12px 12px;
color:#fff;
background:linear-gradient(180deg,rgba(22,22,26,.42),rgba(12,12,14,.34));
border:1px solid rgba(255,255,255,.14);
border-radius:16px;
box-shadow:0 22px 60px rgba(0,0,0,.50);
backdrop-filter:blur(28px) saturate(160%);
-webkit-backdrop-filter:blur(28px) saturate(160%);
transform-origin:90% 100%;
animation:wmeRcSnoozeIn .18s ease-out;
}
@keyframes wmeRcSnoozeIn{from{opacity:0;transform:translate3d(0,8px,0) scale(.985);}to{opacity:1;transform:translate3d(0,0,0) scale(1);}}
.wmeRcSnoozeTitle{
font-weight:900;font-size:12px;letter-spacing:.2px;
opacity:.92;margin:0 0 10px;
}
.wmeRcSnoozeGrid{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:10px;}
.wmeRcSnoozeChip{
cursor:pointer;user-select:none;
padding:8px 12px;border-radius:999px;
border:1px solid rgba(255,255,255,.14);
background:rgba(255,255,255,.05);
color:#fff;font-weight:900;font-size:12px;letter-spacing:.15px;
transition:transform .14s ease, background .14s ease, box-shadow .14s ease, border-color .14s ease;
box-shadow:inset 0 1px 0 rgba(255,255,255,.06);
}
.wmeRcSnoozeChip:hover{
background:rgba(255,255,255,.10);
border-color:rgba(255,255,255,.22);
transform:translate3d(0,-1px,0) scale(1.02);
box-shadow:0 10px 24px rgba(0,0,0,.28), inset 0 1px 0 rgba(255,255,255,.08);
}
.wmeRcSnoozeChip:active{
transform:translate3d(0,0,0) scale(.99);
background:rgba(255,255,255,.12);
}
.wmeRcSnoozeRow{display:flex;gap:8px;align-items:center;}
.wmeRcSnoozeInput{
flex:1;min-width:0;
height:34px;line-height:34px;
border-radius:12px;border:1px solid rgba(255,255,255,.14);
background:rgba(255,255,255,.05);
color:#fff !important;
-webkit-text-fill-color:#fff !important;
padding:0 12px;
font-weight:900;outline:none;
font-variant-numeric:tabular-nums;
transition:box-shadow .14s ease, border-color .14s ease, background .14s ease;
}
.wmeRcSnoozeInput:focus{
border-color:rgba(255,255,255,.24);
background:rgba(255,255,255,.07);
box-shadow:0 0 0 4px rgba(255,255,255,.06);
}
.wmeRcSnoozeInput::placeholder{color:rgba(255,255,255,.28);font-weight:700;}
.wmeRcSnoozeInput:placeholder-shown{color:rgba(255,255,255,.31) !important;-webkit-text-fill-color:#ffffff50 !important;}
.wmeRcSnoozeInput:not(:placeholder-shown){color:#fff !important;-webkit-text-fill-color:#fff !important;}
.wmeRcSnoozeGo{
appearance:none;
height:34px;min-width:70px;
display:flex;align-items:center;justify-content:center;line-height:1;
border-radius:12px;
border:1px solid rgba(var(--pinRGB,255,160,60),.34);
background:var(--wmeRcPinGrad, linear-gradient(180deg, rgba(255,200,120,.92), rgba(255,140,0,.92)));
color:#fff;
padding:0 12px;
font-weight:950;letter-spacing:.15px;
cursor:pointer;
box-shadow:0 10px 26px rgba(0,0,0,.25);
transition:filter .14s ease,transform .14s ease,border-color .14s ease, box-shadow .14s ease;
}
.wmeRcSnoozeGo:hover{
filter:brightness(1.05);
border-color:rgba(var(--pinRGB,255,160,60),.44);
transform:translate3d(0,-1px,0);
box-shadow:0 14px 34px rgba(0,0,0,.30);
}
.wmeRcSnoozeGo:active{
transform:translate3d(0,0,0);
filter:brightness(.98);
}
.wmeRcPinCluster{position:absolute;width:34px;height:34px;border-radius:999px;
display:flex;align-items:center;justify-content:center;
background:rgba(20,20,22,.82);border:1px solid rgba(255,255,255,.14);
box-shadow:0 12px 30px rgba(0,0,0,.40);backdrop-filter:blur(10px);
color:#fff;font-weight:900;font-size:12px;pointer-events:auto;cursor:pointer;
transform:translate3d(-9999px,-9999px,0);will-change:transform;}
.wmeRcPinCluster:hover{background:rgba(24,24,26,.86);}
.wmeRcOlMarker{cursor:pointer !important;position:relative;}
.wmeRcOlMarker img, .wmeRcOlMarker svg, .wmeRcOlMarker canvas{filter:drop-shadow(0 10px 18px rgba(0,0,0,.45));}
.wmeRcPinLabel{
position:absolute;
left:38px;
top:50%;
transform:translate3d(0,-50%,0);
padding:6px 10px;
border-radius:13px;
background: rgba(var(--pinRGB,255,160,60), .34);
border:none;
box-shadow:0 16px 44px rgba(0,0,0,.42), inset 0 0 0 1px rgba(255,255,255,.10);
backdrop-filter:blur(16px) saturate(140%);
-webkit-backdrop-filter:blur(16px) saturate(140%);
color: var(--wmeRcPinLabelFg, #fff);
text-shadow: var(--wmeRcPinLabelShadow, 0 6px 18px rgba(0,0,0,.35));
font:12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
font-weight:600;
letter-spacing:.12px;
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
max-width:220px;
pointer-events:none;
opacity:.98;
}
.wmeRcMenu{position:fixed;z-index:2147483647;min-width:240px;max-width:340px;background:rgba(20,20,22,.72);color:#fff;border:1px solid rgba(255,255,255,.14);border-radius:14px;box-shadow:0 18px 60px rgba(0,0,0,.45);backdrop-filter:blur(18px);overflow:hidden;font:13px/1.25 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;}
.wmeRcMenu.sub{min-width:240px;max-width:340px;border-radius:12px;max-height:380px;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.22) rgba(255,255,255,.06);}
.wmeRcMenu.sub::-webkit-scrollbar{width:10px;}
.wmeRcMenu.sub::-webkit-scrollbar-track{background:rgba(255,255,255,.06);}
.wmeRcMenu.sub::-webkit-scrollbar-thumb{background:rgba(255,255,255,.22);border-radius:10px;border:2px solid rgba(0,0,0,0);background-clip:padding-box;}
.wmeRcMenu.sub::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.30);}
.wmeRcHdr{padding:9px 10px;border-bottom:1px solid rgba(255,255,255,.10);opacity:.96;font-size:12px;display:flex;justify-content:space-between;gap:10px;align-items:center;}
.wmeRcHdrLeft{display:flex;align-items:center;gap:8px;min-width:0;}
.wmeRcHdrTitle{font-weight:900;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcMenu,.wmeRcPins,.wmeRcPins *,.wmeRcPinsPanel,.wmeRcNotice,.wmeRcModal,.wmeRcToast{user-select:none;-webkit-user-select:none;}
.wmeRcInput,.wmeRcSnoozeInput,textarea{user-select:text;-webkit-user-select:text;}
.wmeRcMuted{opacity:.65;}
.wmeRcItem{padding:9px 10px;cursor:pointer;display:flex;justify-content:space-between;gap:10px;align-items:center;transition:.16s ease;}
.wmeRcItem:hover{background:rgba(255,255,255,.08);transform:translate3d(0,-1px,0);}
.wmeRcItem.submenuOpen{background:rgba(255,255,255,.12)!important;box-shadow:none!important;transform:none!important;position:relative;}
.wmeRcItem:active{background:rgba(255,255,255,.12);transform:translate3d(0,0,0);}
.wmeRcItem.disabled{cursor:default;opacity:.55;}
.wmeRcItem.disabled:hover{background:transparent;transform:none;}
.wmeRcItem.sectionHdr{cursor:default;opacity:.70;justify-content:center;}
.wmeRcItem.sectionHdr:hover{background:transparent;transform:none;}
.wmeRcItem.sectionHdr .wmeRcLeft{width:100%;display:flex;justify-content:center;}
.wmeRcItem.sectionHdr .wmeRcLeft>div{font-weight:950;letter-spacing:.2px;opacity:.85;text-align:center;}
.wmeRcItem.sectionHdr > div:last-child{display:none;}
.wmeRcItem.selected{background:rgba(255,255,255,.10);}
.wmeRcLeft{display:flex;flex-direction:column;gap:2px;min-width:0;}
.wmeRcSubText{display:none;}
.wmeRcSep{height:1px;background:rgba(255,255,255,.10);margin:6px 0;}
.wmeRcKbd{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:11px;opacity:.85;border:1px solid rgba(255,255,255,.16);padding:2px 6px;border-radius:8px;height:fit-content;}
.wmeRcChevron{opacity:.8;padding-left:10px;}
.wmeRcCheck{opacity:.95;font-size:13px;}
.wmeRcMiniBtn{cursor:pointer;user-select:none;padding:6px 10px;border-radius:10px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);font-size:12px;line-height:1;white-space:nowrap;transition:.16s ease;}
.wmeRcMiniBtn:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcMiniBtn:active{background:rgba(255,255,255,.14);transform:translate3d(0,0,0);}
.wmeRcMiniIcon{padding:6px 8px;min-width:34px;display:flex;align-items:center;justify-content:center;font-size:14px;}
.wmeRcI{width:16px;height:16px;flex:0 0 16px;display:inline-flex;align-items:center;justify-content:center;opacity:.92;}
.wmeRcI svg{width:16px;height:16px;display:block;fill:currentColor;}
.wmeRcI.big{width:18px;height:18px;flex:0 0 18px;opacity:.95;}
.wmeRcI.big svg{width:18px;height:18px;}
.wmeRcI.xl{width:20px;height:20px;flex:0 0 20px;opacity:.98;}
.wmeRcI.xl svg{width:20px;height:20px;}
.wmeRcLbl{display:flex;align-items:center;gap:8px;}
.wmeRcT{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcSpeedWrap{padding:10px 10px 10px 10px;}
.wmeRcSpeedTitle{font-size:11px;opacity:.75;margin-bottom:7px;display:flex;justify-content:space-between;gap:10px;align-items:center;}
.wmeRcSpeedGrid{display:flex;flex-wrap:wrap;gap:6px;align-items:center;}
.wmeRcSpeedBtn{width:30px;height:30px;border-radius:999px;border:2px solid rgba(255,60,60,.95);background:rgba(255,255,255,.02);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;user-select:none;font-size:11px;line-height:1;transition:.16s ease;position:relative;}
.wmeRcSpeedBtn:hover{background:rgba(255,60,60,.10);transform:translate3d(0,-1px,0);}
.wmeRcSpeedBtn:active{background:rgba(255,60,60,.18);transform:translate3d(0,0,0);}
.wmeRcSpeedBtn svg{width:14px;height:14px;fill:rgba(255,60,60,.95);}
.wmeRcSpeedBtn.sel{background:rgba(255,60,60,.18);box-shadow:0 0 0 4px rgba(255,60,60,.10);}
.wmeRcSpeedBtn.sel::after{content:"";position:absolute;width:6px;height:6px;border-radius:99px;background:rgba(255,255,255,.92);bottom:-2px;right:-2px;box-shadow:0 6px 16px rgba(0,0,0,.35);}
.wmeRcChips{display:flex;gap:6px;}
.wmeRcChip{cursor:pointer;padding:6px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);font-size:12px;opacity:.9;transition:.16s ease;user-select:none;}
.wmeRcChip:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcChip:active{transform:translate3d(0,0,0);}
.wmeRcChip.on{border-color:rgba(255,255,255,.30);background:rgba(255,255,255,.14);opacity:1;}
.wmeRcModalBackdrop{position:fixed;inset:0;z-index:2147483647;background:rgba(0,0,0,.35);opacity:0;transition:opacity .16s ease;will-change:opacity;}
.wmeRcModalBackdrop.show{opacity:1;}
.wmeRcModal{position:fixed;left:50%;top:50%;transform:translate3d(-50%,-50%,0) scale(.985);opacity:0;z-index:2147483647;width:min(560px,calc(100vw - 24px));background:linear-gradient(180deg,rgba(28,28,32,.78),rgba(18,18,20,.78));border:1px solid rgba(255,255,255,.14);border-radius:16px;box-shadow:0 18px 60px rgba(0,0,0,.48);backdrop-filter:blur(12px);color:#fff;font:13px/1.25 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;overflow:hidden;transition:opacity .16s ease,transform .16s ease;will-change:transform,opacity;}
.wmeRcModal.show{opacity:1;transform:translate3d(-50%,-50%,0) scale(1);}
.wmeRcModal:focus,.wmeRcModal:focus-visible,.wmeRcPins:focus,.wmeRcPins:focus-visible{outline:none !important;}
.wmeRcModalHdr{padding:12px 12px;border-bottom:1px solid rgba(255,255,255,.10);display:flex;justify-content:space-between;align-items:center;}
.wmeRcModalTitle{font-weight:800;display:flex;gap:10px;align-items:center;letter-spacing:.2px;white-space:nowrap;}
.wmeRcModalTitle .wmeRcI{width:18px;height:18px;}
.wmeRcModalTitle .wmeRcI svg{width:18px;height:18px;}
.wmeRcModalBody{padding:12px;display:flex;flex-direction:column;gap:10px;}
.wmeRcHint{opacity:.68;font-size:13px;letter-spacing:.1px;}
.wmeRcSwitchLabelWrap{display:flex;align-items:center;gap:8px;}
.wmeRcInfoQ{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:999px;
background:rgba(255,255,255,.10);border:1px solid rgba(255,255,255,.16);
font-weight:700;font-size:12px;opacity:.9;cursor:default;position:relative;flex:0 0 auto;}
.wmeRcInfoQ:hover{background:rgba(255,255,255,.14);}
.wmeRcInfoQ .wmeRcInfoTip{position:absolute;left:calc(100% + 10px);top:50%;transform:translateY(-50%);
min-width:260px;max-width:360px;
background:rgba(18,18,20,.92);border:1px solid rgba(255,255,255,.14);
border-radius:12px;padding:10px 10px;color:rgba(255,255,255,.92);
box-shadow:0 16px 40px rgba(0,0,0,.55);backdrop-filter:blur(16px);
font-size:12px;font-weight:400;line-height:1.25;display:none;z-index:2147483647;cursor:default;}
.wmeRcInfoQ:hover .wmeRcInfoTip,.wmeRcInfoQ:focus .wmeRcInfoTip{display:block;}
.wmeRcModalActions{display:flex;justify-content:flex-end;gap:10px;align-items:center;margin-top:2px;flex-wrap:wrap;}
.wmeRcModalBtn{cursor:pointer;padding:8px 12px;border-radius:12px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);color:#fff;user-select:none;transition:.16s ease;}
.wmeRcModalBtn:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcModalBtn:active{background:rgba(255,255,255,.14);transform:translate3d(0,0,0);}
.wmeRcModalBtn.primary{border-color:rgba(255,255,255,.32);background:rgba(255,255,255,.16);box-shadow:0 0 0 4px rgba(255,255,255,.06);font-weight:700;}
.wmeRcModalBtn.danger{border-color:rgba(255,75,75,.35);background:rgba(255,75,75,.12);}
/* Pins Manager Lightbox */
.wmeRcModal.wmeRcLightbox{width:min(980px,calc(100vw - 24px));height:min(720px,calc(100vh - 24px));}
.wmeRcLightbox .wmeRcModalBody{padding:0;display:flex;flex-direction:row;gap:0;height:100%;}
.wmeRcPmLeft{width:280px;max-width:42vw;border-right:1px solid rgba(255,255,255,.10);padding:12px;display:flex;flex-direction:column;gap:10px;}
.wmeRcPmRight{flex:1;min-width:0;padding:12px;display:flex;flex-direction:column;gap:10px;}
.wmeRcPmHdrRow{display:flex;align-items:center;justify-content:space-between;gap:10px;}
.wmeRcPmSearch{width:100%;}
.wmeRcPmList{flex:1;min-height:0;overflow:auto;display:flex;flex-direction:column;gap:8px;overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:rgba(255,255,255,.22) rgba(255,255,255,.06);}
.wmeRcPmList::-webkit-scrollbar{width:10px;}
.wmeRcPmList::-webkit-scrollbar-track{background:rgba(255,255,255,.06);}
.wmeRcPmList::-webkit-scrollbar-thumb{background:rgba(255,255,255,.22);border-radius:10px;border:2px solid rgba(0,0,0,0);background-clip:padding-box;}
.wmeRcPmCard{position:relative;padding:10px 10px;border-radius:14px;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.04);display:flex;align-items:center;justify-content:space-between;gap:10px;cursor:pointer;transition:.16s ease;}
.wmeRcPmCard:hover{background:rgba(255,255,255,.07);transform:translate3d(0,-1px,0);}
.wmeRcPmCard.on{background:rgba(255,255,255,.10);border-color:rgba(255,255,255,.18);}
.wmeRcPmCardLeft{display:flex;flex-direction:column;gap:2px;min-width:0;}
.wmeRcPmCardTitle{font-weight:900;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcPmCardSub{font-size:12px;opacity:.70;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcPmCardBtns{display:flex;gap:6px;align-items:center;flex:0 0 auto;}
.wmeRcPmTiny{width:28px;height:28px;border-radius:10px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:.16s ease;}
.wmeRcPmTiny:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcPmTiny:active{background:rgba(255,255,255,.14);transform:translate3d(0,0,0);}
.wmeRcPmRow{padding:10px 10px;border-radius:14px;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.04);display:flex;align-items:center;justify-content:space-between;gap:10px;}
.wmeRcPmRowLeft{display:flex;flex-direction:column;gap:2px;min-width:0;}
.wmeRcPmRowTitle{font-weight:900;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%;}
.wmeRcPmRowSub{font-size:12px;opacity:.70;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcPmRowBtns{display:flex;gap:6px;align-items:center;flex:0 0 auto;}
.wmeRcPmSelect{min-width:160px;max-width:220px;}
.wmeRcWizardHeader{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;}
.wmeRcWizardHdrLeft{display:flex;flex-direction:column;gap:2px;min-width:0;}
.wmeRcWizardTitle{font-weight:900;letter-spacing:.2px;}
.wmeRcWizardSub{font-size:12px;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:360px;}
.wmeRcWizardDots{display:flex;gap:6px;align-items:center;flex-wrap:nowrap;}
.wmeRcDot{width:8px;height:8px;border-radius:999px;background:rgba(255,255,255,.18);box-shadow:inset 0 0 0 1px rgba(255,255,255,.10);}
.wmeRcDot.on{background:rgba(255,255,255,.70);box-shadow:0 0 0 4px rgba(255,255,255,.10);}
.wmeRcDot.done{background:rgba(255,255,255,.36);}
.wmeRcWizardSection{display:flex;flex-direction:column;gap:10px;}
.wmeRcWizardHint{font-size:12px;opacity:.75;}
.wmeRcWizardGrid{display:flex;flex-wrap:wrap;gap:8px;align-items:center;}
.wmeRcWizardBtn{cursor:pointer;padding:8px 12px;border-radius:14px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);transition:.16s ease;user-select:none;}
.wmeRcWizardBtn:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcWizardBtn:active{background:rgba(255,255,255,.14);transform:translate3d(0,0,0);}
.wmeRcWizardBtn.on{border-color:rgba(255,255,255,.30);background:rgba(255,255,255,.14);box-shadow:0 0 0 4px rgba(255,255,255,.08);}
.wmeRcWizardList{display:flex;flex-direction:column;gap:8px;}
.wmeRcWizardRow{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 10px;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.04);border-radius:14px;}
.wmeRcWizardRowLeft{display:flex;flex-direction:column;gap:2px;min-width:0;}
.wmeRcWizardRowTitle{font-weight:800;}
.wmeRcWizardRowSub{font-size:12px;opacity:.7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcWizardTri{display:flex;gap:6px;align-items:center;flex-wrap:nowrap;}
.wmeRcWizardPill{cursor:pointer;padding:6px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);font-size:12px;opacity:.95;transition:.16s ease;user-select:none;}
.wmeRcWizardPill:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcWizardPill:active{background:rgba(255,255,255,.14);transform:translate3d(0,0,0);}
.wmeRcWizardPill.on{border-color:rgba(255,255,255,.30);background:rgba(255,255,255,.14);box-shadow:0 0 0 4px rgba(255,255,255,.08);}
.wmeRcInput{width:100%;padding:11px 14px;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.05);color:#fff;outline:none;font:13px/1.25 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;box-shadow:inset 0 1px 0 rgba(255,255,255,.05);}
.wmeRcRow{display:flex;gap:10px;}
.wmeRcEmojiRow{align-items:stretch;}
.wmeRcEmojiRow .wmeRcEmojiBtn{height:auto;}
.wmeRcEmojiRow .wmeRcLenHost{align-self:stretch;}
.wmeRcSoundPick{position:relative;flex:1 1 auto;min-width:200px;outline:none;}
.wmeRcSoundBtn{height:38px;border-radius:14px;border:1px solid rgba(255,255,255,.14);background:rgba(0,0,0,.18);display:flex;align-items:center;justify-content:space-between;gap:10px;padding:0 12px;cursor:pointer;user-select:none;}
.wmeRcSoundBtnLabel{color:rgba(255,255,255,.92);font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcSoundCaret{display:flex;align-items:center;justify-content:center;opacity:.9;}
.wmeRcSoundCaret svg{width:16px;height:16px;fill:rgba(255,255,255,.85);}
.wmeRcSoundMenu{position:absolute;left:0;right:0;top:42px;background:rgba(18,18,20,.86);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border:1px solid rgba(255,255,255,.16);border-radius:14px;box-shadow:0 20px 60px rgba(0,0,0,.50);padding:6px;display:none;max-height:240px;overflow:auto;z-index:99999;}
.wmeRcSoundPick[data-open="1"] .wmeRcSoundMenu{display:block;}
/* Sound menu portal (so it isn't clipped by modal overflow) */
.wmeRcSoundMenu.wmeRcSoundMenuPortal{position:fixed;left:auto;right:auto;top:auto;width:auto;z-index:2147483647;}
.wmeRcSoundItem{padding:9px 10px;border-radius:10px;color:rgba(255,255,255,.92);font-size:13px;cursor:pointer;user-select:none;transition:.14s ease;}
.wmeRcSoundItem:hover{background:rgba(255,255,255,.10);}
.wmeRcSoundMenu::-webkit-scrollbar{width:0;height:0;}
.wmeRcSoundMenu{scrollbar-width:none;}
.wmeRcEmojiBtn{flex:0 0 auto;width:40px;height:40px;border-radius:14px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);color:#fff;font-size:18px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:.15s ease;box-shadow:inset 0 1px 0 rgba(255,255,255,.06);padding:0;margin:0;line-height:1;}
.wmeRcEmojiBtn:hover{background:rgba(255,255,255,.10);}
.wmeRcNoEmojiIcon{position:relative;display:inline-flex;align-items:center;justify-content:center;filter:grayscale(1);opacity:.55;font-size:18px;line-height:1;}
.wmeRcNoEmojiIcon::after{content:"";position:absolute;width:130%;height:2px;background:rgba(255,255,255,.45);transform:rotate(45deg);border-radius:2px;}
.wmeRcNoEmojiIconSm{font-size:16px;}
.wmeRcEmojiNone{font-size:12px;opacity:.78;letter-spacing:.1px;}
.wmeRcEmojiItemNone{font-size:12px;opacity:.9;}
.wmeRcEmojiPop{margin-top:10px;border-radius:14px;border:1px solid rgba(255,255,255,.12);background:rgba(20,20,22,.70);backdrop-filter:blur(14px);padding:10px;}
.wmeRcEmojiPop.hidden{display:none;}
.wmeRcEmojiGrid{display:grid;grid-template-columns:repeat(10, 1fr);gap:6px;}
.wmeRcEmojiItem{width:100%;aspect-ratio:1/1;border-radius:10px;border:1px solid rgba(255,255,255,.10);background:rgba(255,255,255,.05);color:#fff;font-size:16px;display:flex;align-items:center;justify-content:center;cursor:pointer;transition:.12s ease;}
.wmeRcEmojiItem:hover{background:rgba(255,255,255,.10);transform:translateY(-1px);}
.wmeRcInput::-webkit-calendar-picker-indicator{filter:invert(1);opacity:.85;cursor:pointer;}
.wmeRcLayerRow{padding:4px 2px;display:flex;align-items:center;}
.wmeRcLayerLabel{display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none;opacity:.92;}
.wmeRcLayerLabel input{transform:translateY(1px);}
.wmeRcInput::placeholder{color:rgba(255,255,255,.28);}
.wmeRcModal .wmeRcInput{color:#fff !important;-webkit-text-fill-color:#fff !important;caret-color:#fff !important;}
.wmeRcModal .wmeRcInput::placeholder{color:rgba(255,255,255,.30) !important;}
.wmeRcModal .wmeRcInput:placeholder-shown{color:rgba(255,255,255,.31) !important;-webkit-text-fill-color:#ffffff50 !important;}
.wmeRcModal .wmeRcInput:not(:placeholder-shown){color:#fff !important;-webkit-text-fill-color:#fff !important;}
.wmeRcInput.bad{border-color:rgba(255,75,75,.35);background:rgba(255,70,70,.07);box-shadow:0 0 0 2px rgba(255,75,75,.12), inset 0 1px 0 rgba(255,255,255,.06);}
.wmeRcInput:focus{border-color:rgba(255,255,255,.30);box-shadow:0 0 0 4px rgba(255,255,255,.07),inset 0 1px 0 rgba(255,255,255,.06);}
.wmeRcToggleGrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;}
.wmeRcToggleBtn{cursor:pointer;padding:10px 10px;border-radius:14px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:space-between;gap:10px;user-select:none;transition:.16s ease;}
.wmeRcToggleBtn:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcToggleBtn:active{transform:translate3d(0,0,0);}
.wmeRcToggleBtn.off{border-color:rgba(255,75,75,.35);background:rgba(255,75,75,.08);}
.wmeRcToggleBtn.on{border-color:rgba(60,255,143,.35);background:rgba(60,255,143,.08);}
.wmeRcToggleLeft{display:flex;flex-direction:column;gap:2px;min-width:0;}
.wmeRcToggleName{font-weight:800;}
.wmeRcToggleDesc{font-size:11px;opacity:.70;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcPill{font-size:11px;border:1px solid rgba(255,255,255,.18);padding:2px 8px;border-radius:999px;opacity:.95;}
.wmeRcPill.on{border-color:rgba(60,255,143,.35);background:rgba(60,255,143,.12);}
.wmeRcPill.off{border-color:rgba(255,75,75,.35);background:rgba(255,75,75,.12);}
.wmeRcActionList{display:flex;flex-direction:column;gap:8px;}
.wmeRcActionCard{cursor:pointer;padding:10px 10px;border-radius:14px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:space-between;gap:10px;user-select:none;transition:.16s ease;}
.wmeRcActionCard:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcActionCard:active{background:rgba(255,255,255,.14);transform:translate3d(0,0,0);}
.wmeRcActionCard.disabled{cursor:default;opacity:.55;}
.wmeRcActionCard.disabled:hover{background:rgba(255,255,255,.06);transform:none;}
.wmeRcActionLeft{display:flex;flex-direction:column;gap:2px;min-width:0;}
.wmeRcActionTitle{font-weight:800;display:flex;gap:8px;align-items:center;}
.wmeRcActionDesc{font-size:11px;opacity:.70;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcSideWrap{padding:10px 12px;font-family:system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:#111;}
.wmeRcSideCard{background:#fff;border:1px solid rgba(0,0,0,.10);border-radius:12px;box-shadow:0 10px 30px rgba(0,0,0,.08);padding:10px 10px;}
.wmeRcSideRow{display:flex;align-items:center;justify-content:space-between;gap:12px;}
.wmeRcSideTitle{font-weight:800;}
.wmeRcSideSub{font-size:12px;opacity:.65;margin-top:2px;}
.wmeRcSwitch{width:44px;height:26px;border-radius:999px;border:1px solid rgba(0,0,0,.18);background:rgba(0,0,0,.08);position:relative;cursor:pointer;transition:.16s ease;flex:0 0 auto;}
.wmeRcSwitch.on{background:rgba(60,255,143,.35);border-color:rgba(60,255,143,.55);}
.wmeRcKnob{width:22px;height:22px;border-radius:999px;background:#fff;position:absolute;top:1px;left:1px;box-shadow:0 8px 16px rgba(0,0,0,.18);transition:.16s ease;}
.wmeRcSwitch.on .wmeRcKnob{left:21px;}
.wmeRcTinyToggle{position:fixed;left:12px;bottom:12px;z-index:2147483646;background:rgba(20,20,22,.78);color:#fff;border:1px solid rgba(255,255,255,.18);border-radius:999px;padding:8px 10px;backdrop-filter:blur(10px);font:12px/1.1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;display:flex;gap:8px;align-items:center;cursor:pointer;user-select:none;opacity:.35;transition:.16s ease;}
.wmeRcTinyToggle:hover{opacity:1;transform:translate3d(0,-1px,0);}
.wmeRcTinyDot{width:10px;height:10px;border-radius:99px;background:#3cff8f;}
.wmeRcTinyDot.off{background:#ff4b4b;}
.wmeRcDiffBox{border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.04);border-radius:14px;padding:10px 10px;display:flex;flex-direction:column;gap:8px;}
.wmeRcDiffRow{display:flex;align-items:center;justify-content:space-between;gap:12px;}
.wmeRcDiffLeft{display:flex;flex-direction:column;gap:2px;min-width:0;}
.wmeRcDiffName{font-weight:800;}
.wmeRcDiffDesc{font-size:11px;opacity:.72;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcDiffRight{font:12px/1.2 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;opacity:.9;white-space:nowrap;}
.wmeRcTag{font-size:11px;border:1px solid rgba(255,255,255,.18);padding:2px 8px;border-radius:999px;opacity:.92;}
.wmeRcTag.ok{border-color:rgba(60,255,143,.35);background:rgba(60,255,143,.12);}
.wmeRcTag.warn{border-color:rgba(255,188,75,.35);background:rgba(255,188,75,.10);}
.wmeRcTag.bad{border-color:rgba(255,75,75,.35);background:rgba(255,75,75,.12);}
.wmeRcPins{position:absolute;left:12px;top:12px;z-index:2147483646;width:260px;max-width:calc(100vw - 24px);
background:rgba(20,20,22,.52);color:#fff;border:1px solid rgba(255,255,255,.12);border-radius:14px;
box-shadow:0 16px 48px rgba(0,0,0,.35);backdrop-filter:blur(14px);
overflow:hidden;font:12px/1.25 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
display:flex;flex-direction:column;resize:vertical;min-height:140px;max-height:calc(100vh - 24px);}
.wmeRcPins.hidden{display:none;}
.wmeRcPins.collapsed{resize:none;min-height:0 !important;}
.wmeRcPins.collapsed .wmeRcPinsList{display:none;}
/* Pins bubble (minimized state) */
.wmeRcPinsBubble{
position:absolute;
width:46px;height:46px;
border-radius:999px;
border:1px solid rgba(255,255,255,.16);
background:rgba(20,20,22,.55);
color:#fff;
backdrop-filter:blur(18px) saturate(150%);
-webkit-backdrop-filter:blur(18px) saturate(150%);
box-shadow:0 18px 54px rgba(0,0,0,.38);
display:flex;align-items:center;justify-content:center;
cursor:grab;
user-select:none;-webkit-user-select:none;
opacity:0;
transform:scale(.92) translate3d(0,8px,0);
transition:opacity .22s ease, transform .22s cubic-bezier(.2,.9,.2,1), box-shadow .22s ease;
z-index:2147483646;
pointer-events:auto;
}
.wmeRcPinsBubble.show{opacity:1;transform:scale(1) translate3d(0,0,0);}
.wmeRcPinsBubble:active{cursor:grabbing;transform:scale(.98) translate3d(0,0,0);}
.wmeRcPinsBubble svg{width:30px;height:30px;display:block;fill:currentColor;}
.wmeRcPinsBubblePulse{animation:wmeRcPinsBubblePulse 2.6s ease-in-out infinite;}
@keyframes wmeRcPinsBubblePulse{
0%,100%{box-shadow:0 18px 54px rgba(0,0,0,.38);}
50%{box-shadow:0 22px 70px rgba(0,0,0,.52);}
}
/* Pins panel open/close animation helpers */
.wmeRcPins.animOut{opacity:0;transform:scale(.94) translate3d(0,10px,0);pointer-events:none;}
.wmeRcPins.animIn{opacity:1;transform:scale(1) translate3d(0,0,0);}
.wmeRcPinsHdr{padding:9px 10px;border-bottom:1px solid rgba(255,255,255,.10);display:flex;align-items:center;justify-content:space-between;gap:10px;cursor:move;}
.wmeRcPinsTitle{font-weight:900;letter-spacing:.2px;white-space:nowrap;}
.wmeRcPinsCount{opacity:.70;font-size:11px;white-space:nowrap;}
.wmeRcPinsHdrLeft{display:flex;align-items:center;gap:8px;min-width:0;}
.wmeRcPinsHdrRight{display:flex;align-items:center;gap:8px;}
.wmeRcPinsHdrBtn{width:28px;height:28px;border-radius:10px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:.16s ease;}
.wmeRcPinsHdrBtn:hover{background:rgba(255,255,255,.10);}
.wmeRcPinsFilter{max-width:140px;}
.wmeRcSelect{border-radius:12px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.05);color:#fff;outline:none;padding:7px 10px;font-weight:800;font-size:12px;box-shadow:inset 0 1px 0 rgba(255,255,255,.05);}
.wmeRcSelect:focus{border-color:rgba(255,255,255,.30);box-shadow:0 0 0 4px rgba(255,255,255,.07),inset 0 1px 0 rgba(255,255,255,.06);}
.wmeRcSelect option{background:#0b0b0e;color:#fff;}
.wmeRcPinsEmpty{padding:12px 10px;opacity:.7;font-size:12px;}
.wmeRcPinsList{flex:1 1 auto;min-height:0;overflow:auto;overflow-x:hidden;overscroll-behavior:contain;scrollbar-width:none;-ms-overflow-style:none;}
.wmeRcPinsList::-webkit-scrollbar{width:0;height:0;}
.wmeRcPinsGroup{border-top:1px solid rgba(255,255,255,.08);}
.wmeRcPinsGroup:first-child{border-top:none;}
/* Keep the current folder header visible while scrolling through many pins */
.wmeRcPinsGroupHdr{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:8px 10px;font-weight:900;letter-spacing:.2px;
user-select:none;position:sticky;top:0;z-index:3;}
.wmeRcPinsGroupHdr.wmeRcSticky{background:rgba(0,0,0,.18);}
.wmeRcPinsGroupName{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:190px;}
.wmeRcPinsGroupCount{opacity:.65;font-size:11px;flex:0 0 auto;}
.wmeRcPinsGroupHdr.dropTarget{background:rgba(255,255,255,.09);}
.wmeRcPinsGroupHdr{cursor:pointer;}
.wmeRcPinsGroup.collapsed .wmeRcPinsGroupBody{display:none;}
.wmeRcPinsGroupTop{display:flex;align-items:center;gap:8px;min-width:0;}
.wmeRcPinsGroupTwisty{width:18px;height:18px;display:flex;align-items:center;justify-content:center;opacity:.75;flex:0 0 auto;transition:transform .16s ease,opacity .16s ease;}
.wmeRcPinsGroupEmoji{width:18px;height:18px;display:flex;align-items:center;justify-content:center;flex:0 0 auto;font-size:14px;opacity:.92;filter:drop-shadow(0 10px 18px rgba(0,0,0,.30));}
.wmeRcPinsGroupTwisty svg{width:14px;height:14px;display:block;}
.wmeRcPinsGroup.collapsed .wmeRcPinsGroupTwisty{transform:rotate(-90deg);opacity:.60;}
.wmeRcPinsGroupActions{display:flex;align-items:center;gap:6px;flex:0 0 auto;}
.wmeRcPinsGroupBtn{width:24px;height:24px;border-radius:9px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:.16s ease;opacity:.0;pointer-events:none;}
.wmeRcPinsGroupHdr:hover .wmeRcPinsGroupBtn{opacity:1;pointer-events:auto;}
.wmeRcPinsGroupBtn:hover{background:rgba(255,255,255,.10);}
.wmeRcPinsGroupBody{padding:2px 6px 8px 6px;display:flex;flex-direction:column;gap:6px;}
.wmeRcPinsGroupEmpty{padding:8px 6px;color:rgba(255,255,255,.55);font-size:11px;border:1px dashed rgba(255,255,255,.14);border-radius:12px;text-align:center;background:rgba(255,255,255,.02);}
/* Map Pins markers (map) */
.wmeRcPinMarkers{position:absolute;inset:0;z-index:2147483645;pointer-events:none;}
.wmeRcPinMarker{position:absolute;width:20px;height:20px;transform:translate3d(-9999px,-9999px,0);pointer-events:auto;cursor:pointer;will-change:transform;}
.wmeRcPinMarker::before{
content:"";
position:absolute;left:50%;top:0;
width:16px;height:16px;border-radius:999px;
transform:translateX(-50%);
background:var(--wmeRcPinGrad, linear-gradient(180deg, rgba(255,200,120,.95), rgba(255,140,0,.95)));
box-shadow:0 10px 24px rgba(0,0,0,.35);
}
.wmeRcPinMarker::after{
content:"";
position:absolute;left:50%;top:12px;
width:0;height:0;
transform:translateX(-50%);
border-left:6px solid transparent;
border-right:6px solid transparent;
border-top:10px solid var(--wmeRcPinTip, rgba(255,140,0,.95));
filter:drop-shadow(0 10px 18px rgba(0,0,0,.28));
}
.wmeRcPinMarkerInner{
position:absolute;left:50%;top:5px;width:6px;height:6px;border-radius:999px;
transform:translateX(-50%);
background:rgba(20,20,22,.75);
box-shadow:inset 0 1px 0 rgba(255,255,255,.08);
pointer-events:none;
}
.wmeRcPinMarker:hover::before{filter:brightness(1.05);}
.wmeRcPinMarkerActive::before{
box-shadow:0 12px 26px rgba(0,0,0,.35), 0 0 18px var(--wmeRcPinGlow, rgba(255,160,60,.34)), 0 0 36px var(--wmeRcPinGlow2, rgba(255,160,60,.22));
animation:wmeRcMarkerBreath 1.7s ease-in-out infinite;
}
@keyframes wmeRcMarkerBreath{
0%,100%{filter:brightness(1); }
50%{filter:brightness(1.08); }
}
/* Reminder picker (clock) */
.wmeRcTabs{display:flex;gap:6px;align-items:center;}
.wmeRcTab{cursor:pointer;padding:7px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);font-size:12px;opacity:.90;user-select:none;transition:.16s ease;}
.wmeRcTab:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcTab:active{transform:translate3d(0,0,0);}
.wmeRcTab.on{border-color:rgba(255,255,255,.30);background:rgba(255,255,255,.14);box-shadow:0 0 0 4px rgba(255,255,255,.08);opacity:1;}
.wmeRcClockWrap{display:flex;gap:12px;align-items:center;justify-content:space-between;flex-wrap:wrap;}
.wmeRcClock{width:140px;height:140px;border-radius:999px;position:relative;flex:0 0 auto;
background:radial-gradient(circle at 30% 25%, rgba(255,255,255,.18), rgba(255,255,255,.02) 55%, rgba(0,0,0,.10));
border:1px solid rgba(255,255,255,.14);
box-shadow:inset 0 0 0 1px rgba(255,255,255,.06), 0 16px 48px rgba(0,0,0,.35);
backdrop-filter:blur(10px);
user-select:none;
}
.wmeRcClock::after{content:"";position:absolute;inset:9px;border-radius:999px;border:1px dashed rgba(255,255,255,.10);opacity:.9;pointer-events:none;}
.wmeRcClockCenter{position:absolute;left:50%;top:50%;width:9px;height:9px;border-radius:999px;background:rgba(255,255,255,.88);transform:translate(-50%,-50%);box-shadow:0 10px 22px rgba(0,0,0,.35);pointer-events:none;}
.wmeRcClockHand{position:absolute;left:50%;top:50%;width:2px;height:54px;background:linear-gradient(180deg, rgba(255,255,255,.95), rgba(255,255,255,.35));
transform-origin:50% 100%;
transform:translate(-50%,-100%) rotate(0deg);
border-radius:999px;
transition:transform .18s ease;
filter:drop-shadow(0 10px 12px rgba(0,0,0,.35));
pointer-events:none;
}
.wmeRcClockHand::after{content:"";position:absolute;top:-6px;left:50%;width:8px;height:8px;border-radius:999px;background:rgba(255,255,255,.92);transform:translateX(-50%);box-shadow:0 10px 20px rgba(0,0,0,.35);}
.wmeRcClockHand.min{height:54px;}
.wmeRcClockHand.hour{width:3px;height:38px;opacity:.95;filter:drop-shadow(0 10px 10px rgba(0,0,0,.32));}
.wmeRcClockHand.sec{display:none;width:1px;height:70px;opacity:.75;pointer-events:none;
background:linear-gradient(180deg, rgba(255,255,255,.70), rgba(255,255,255,.18));
filter:drop-shadow(0 10px 10px rgba(0,0,0,.28));
}
.wmeRcClockHand.sec::after{width:6px;height:6px;top:-5px;opacity:.75;}
.wmeRcPinCountdown{font-variant-numeric:tabular-nums; letter-spacing:.2px; margin-left:6px;}
.wmeRcPinSubLine{opacity:.86;}
.wmeRcPinCountdownLine{margin-top:2px; font-variant-numeric:tabular-nums; letter-spacing:.2px;}
.wmeRcPinCountdownBadge{display:inline-flex;align-items:center;justify-content:center;
padding:4px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.18);
background:rgba(255,255,255,.06);box-shadow:inset 0 1px 0 rgba(255,255,255,.06);
font:700 12px/1 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;min-width:56px;contain:layout paint;color:rgba(255,255,255,.86);
cursor:pointer;}
.wmeRcPinCountdownBadge:empty{display:none;}
.wmeRcColorRow{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin-top:2px;}
.wmeRcColorSwatch{width:22px;height:22px;border-radius:10px;border:1px solid rgba(255,255,255,.18);background:var(--c);cursor:pointer;box-shadow:0 10px 18px rgba(0,0,0,.22);}
.wmeRcColorSwatch.sel{box-shadow:0 0 0 3px rgba(255,255,255,.14), 0 10px 18px rgba(0,0,0,.22);outline:2px solid rgba(255,255,255,.26);outline-offset:2px;}
.wmeRcColorPicker{appearance:none;-webkit-appearance:none;width:32px;height:22px;border-radius:10px;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);padding:0;cursor:pointer;}
.wmeRcColorPicker::-webkit-color-swatch-wrapper{padding:0;}
.wmeRcColorPicker::-webkit-color-swatch{border:none;border-radius:9px;}
.wmeRcColorPlus{width:32px;height:22px;border-radius:11px;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;cursor:pointer;user-select:none}
.wmeRcColorPlus:hover{background:rgba(255,255,255,.10)}
.wmeRcColorSwatch.custom{box-shadow:0 0 0 1px rgba(255,255,255,.10) inset}
.wmeRcColorSep{width:auto; padding:0 6px; color:rgba(255,255,255,.32); font:700 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif; user-select:none;}
.wmeRcColorPop{
position:fixed; z-index:2147483648;
width:420px;
max-width:calc(100vw - 18px);
border-radius:20px;
border:1px solid rgba(255,255,255,.10);
background:rgba(12,15,21,.86);
backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px);
box-shadow:0 26px 80px rgba(0,0,0,.62);
padding:14px 14px 12px;
user-select:none;
}
.wmeRcColorPop *{box-sizing:border-box;}
.wmeRcColorTopRow{display:flex; gap:10px; align-items:center; margin-top:6px; margin-bottom:10px;}
.wmeRcColorSwatchBig{width:34px; height:34px; border-radius:12px; border:1px solid rgba(255,255,255,.14);
background:var(--c); box-shadow:0 14px 26px rgba(0,0,0,.32);
}
.wmeRcColorStats{display:flex; flex-direction:column; gap:4px; flex:1 1 auto; min-width:0;}
.wmeRcColorStatLine{display:flex; gap:12px; flex-wrap:wrap; align-items:baseline; color:rgba(255,255,255,.86);
font:600 12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
}
.wmeRcColorStatLine span{display:inline-flex; gap:6px; align-items:baseline;}
.wmeRcColorStatKey{opacity:.55; font-weight:700; letter-spacing:.2px;}
.wmeRcColorStatVal{font-variant-numeric:tabular-nums; letter-spacing:.2px;}
.wmeRcPickerRow{display:flex; gap:12px; align-items:stretch; padding-top:6px;}
.wmeRcPickerSV{
position: relative;
width: 220px;
height: 180px;
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, .12);
overflow: hidden;
background:linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0)),linear-gradient(to right, rgba(255,255,255,1), rgba(255,255,255,0)),var(--hue);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .06), 0 18px 34px rgba(0, 0, 0, .28);
touch-action: none;
}
.wmeRcPickerSV::before{content:""; position:absolute; inset:0;
background:linear-gradient(to right, rgba(255,255,255,1), rgba(255,255,255,0));
}
.wmeRcPickerSV::after{content:""; position:absolute; inset:0;
background:linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0));
}
.wmeRcPickerSV::before,
.wmeRcPickerSV::after{content:none !important;}
.wmeRcPickerSVDot{
position:absolute;
width:14px; height:14px;
border-radius:999px;
border:2px solid rgba(255,255,255,.92);
box-shadow:0 10px 20px rgba(0,0,0,.55);
transform:translate(-7px,-7px);
pointer-events:none;
}
.wmeRcPickerHue{
position:relative;
width:16px; height:180px;
border-radius:999px;
border:1px solid rgba(255,255,255,.12);
overflow:hidden;
box-shadow:inset 0 1px 0 rgba(255,255,255,.06), 0 18px 34px rgba(0,0,0,.28);
touch-action:none;
background:linear-gradient(to bottom,
#ff0000 0%,
#ffff00 17%,
#00ff00 33%,
#00ffff 50%,
#0000ff 67%,
#ff00ff 83%,
#ff0000 100%
);
}
/* FIX8: remove borders around the color picker controls (SV square + hue bar) */
.wmeRcPickerSV,
.wmeRcPickerHue{
border:none !important;
}
.wmeRcPickerSV,.wmeRcPickerHue{outline:none !important;}
.wmeRcPickerHueThumb{
position:absolute; left:50%;
width:22px; height:6px;
border-radius:999px;
background:rgba(255,255,255,.92);
box-shadow:0 10px 18px rgba(0,0,0,.55);
transform:translate(-50%,-50%);
pointer-events:none;
}
.wmeRcPickerFields{flex:1 1 auto; display:flex; flex-direction:column; gap:8px; min-width:0;}
.wmeRcPickerFieldRow{
display:flex;
align-items:center;
gap:8px;
min-width:0;
}
.wmeRcPickerFieldLbl,
.wmeRcPickerFieldRow label{
width:34px;
flex:0 0 34px;
color:rgba(255,255,255,.50);
font:800 11px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
letter-spacing:.3px;
}
.wmeRcPickerField{
flex:1 1 auto;
width:100%;
min-width:0;
height:34px;
border-radius:12px;
border:1px solid rgba(255,255,255,.12);
background:rgba(255,255,255,.06);
color:#fff;
outline:none;
padding:0 10px;
font:600 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
box-shadow:inset 0 1px 0 rgba(255,255,255,.06);
color:#fff !important;
-webkit-text-fill-color:#fff !important;
caret-color:#fff !important;
opacity:1 !important;
visibility:visible !important;
}
.wmeRcPickerField:focus{
border-color:rgba(255,255,255,.28);
background:rgba(255,255,255,.09);
box-shadow:0 0 0 3px rgba(124,92,255,.18), inset 0 1px 0 rgba(255,255,255,.08);
}
.wmeRcPickerFieldRow:focus-within .wmeRcPickerFieldLbl,
.wmeRcPickerFieldRow:focus-within label{
color:rgba(255,255,255,.72);
}
.wmeRcPickerField::placeholder{-webkit-text-fill-color:rgba(255,255,255,.36); color:rgba(255,255,255,.36);}
.wmeRcColorActions{display:flex; align-items:center; justify-content:flex-end; gap:10px; padding-top:12px;}
.wmeRcColorSavePreset{
height:34px; padding:0 14px; border-radius:999px;
border:1px solid rgba(255,255,255,.12);
background:rgba(255,255,255,.06);
color:rgba(255,255,255,.86);
cursor:pointer; user-select:none;
display:flex; align-items:center; justify-content:center;
font:700 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
letter-spacing:.2px;
}
.wmeRcColorSavePreset:hover{background:rgba(255,255,255,.10);}
.wmeRcColorDone{
height:34px; padding:0 14px; border-radius:999px;
border:1px solid rgba(255,255,255,.12);
background:rgba(255,255,255,.08);
color:#fff;
cursor:pointer; user-select:none;
font:800 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
box-shadow:0 18px 44px rgba(0,0,0,.42);
}
.wmeRcColorDone:hover{filter:brightness(1.04);}
.wmeRcColorDelete{
height:34px; padding:0 14px; border-radius:999px;
border:1px solid rgba(255,110,110,.22);
background:rgba(255,110,110,.10);
color:rgba(255,255,255,.92);
cursor:pointer; user-select:none;
font:800 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
box-shadow:0 18px 44px rgba(0,0,0,.32);
}
.wmeRcColorDelete:hover{filter:brightness(1.06);}
.wmeRcSwatchEditTip{
position:fixed;
z-index:2147483649;
padding:6px;
border-radius:999px;
border:1px solid rgba(255,255,255,.14);
background:rgba(20,24,32,.72);
color:rgba(255,255,255,.90);
user-select:none;
backdrop-filter:blur(14px);
-webkit-backdrop-filter:blur(14px);
box-shadow:0 18px 44px rgba(0,0,0,.45);
font:500 11px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
display:inline-flex;
flex-direction:row;
flex-wrap:nowrap;
width:max-content;
white-space:nowrap;
gap:6px;
align-items:center;
}
.wmeRcSwatchEditTipBtn{
width:28px;
height:28px;
border-radius:999px;
border:none;
background:transparent;
color:rgba(255,255,255,.90);
cursor:pointer;
display:grid;
place-items:center;
padding:0;
}
.wmeRcSwatchEditTipBtn:hover{background:rgba(255,255,255,.10);}
.wmeRcSwatchEditTipBtn:active{background:rgba(255,255,255,.16);}
.wmeRcSwatchEditTipBtn svg{width:14px;height:14px;display:block;opacity:.92;}
.wmeRcColorDone:active{transform:translate3d(0,1px,0);}
.wmeRcPinRowActive{
background:linear-gradient(180deg, rgba(255,255,255,.10), rgba(255,255,255,.06));
will-change:box-shadow,background;
animation:wmeRcPinBreath 1.7s ease-in-out infinite;
}
@keyframes wmeRcPinBreath{
0%,100%{box-shadow:inset 0 0 0 999px rgba(255,160,60,.06), inset 0 0 22px rgba(255,160,60,.22);}
50%{box-shadow:inset 0 0 0 999px rgba(255,160,60,.10), inset 0 0 34px rgba(255,160,60,.34);}
}
.wmeRcPinLeft{cursor:grab;}
.wmeRcPinRow.dragging .wmeRcPinLeft{cursor:grabbing;}
.wmeRcPinBtns{
cursor:default;
display:flex;
align-items:center;
justify-content:flex-end;
gap:6px;
position:relative;
width:28px;
height:28px;
flex:0 0 auto;
transition:width .22s cubic-bezier(.2,.9,.2,1);
}
.wmeRcPinRow:hover .wmeRcPinBtns{width:160px;}
.wmeRcPinCountdown.bell{display:inline-flex; align-items:center; gap:6px; animation:wmeRcBellPulse 1.2s ease-in-out infinite;}
@keyframes wmeRcBellPulse{0%,100%{transform:scale(1);}50%{transform:scale(1.08);}}
.wmeRcClockTicks{position:absolute;inset:0;pointer-events:none;opacity:.55;}
.wmeRcClockTick{position:absolute;left:50%;top:50%;width:2px;height:6px;background:rgba(255,255,255,.35);transform-origin:50% 68px;border-radius:999px;}
.wmeRcClockNums{position:absolute;inset:0;pointer-events:none;opacity:.80;}
.wmeRcClockNum{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);font:700 11px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;letter-spacing:.2px;color:rgba(255,255,255,.82);text-shadow:0 10px 22px rgba(0,0,0,.35);}
.wmeRcClockValue{display:flex;flex-direction:column;gap:6px;min-width:200px;flex:1 1 auto;}
.wmeRcBigValue{font:800 28px/1.05 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;letter-spacing:.3px;}
.wmeRcEditableValue{cursor:text;display:inline-flex;align-items:baseline;gap:8px;width:fit-content;padding-bottom:2px;border-bottom:1px dashed rgba(255,255,255,.32);}
.wmeRcEditableValue::after{content:"EDIT";font:900 10px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;letter-spacing:.6px;text-transform:uppercase;opacity:.55;padding:3px 6px;border-radius:999px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);}
.wmeRcEditableValue:hover{border-bottom-color:rgba(255,255,255,.60);}
.wmeRcEditableValue:focus{outline:none;box-shadow:0 0 0 2px rgba(255,255,255,.14);}
.wmeRcBigValueSub{opacity:.72;font-size:12px;}
.wmeRcStepper{display:flex;gap:8px;align-items:center;flex-wrap:wrap;}
.wmeRcStepBtn{cursor:pointer;padding:8px 10px;border-radius:12px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);transition:.16s ease;user-select:none;}
.wmeRcStepBtn:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcStepBtn:active{transform:translate3d(0,0,0);}
.wmeRcQuickRow{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin-top:2px;}
.wmeRcQuick{cursor:pointer;padding:7px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.06);font-size:12px;opacity:.92;transition:.16s ease;user-select:none;}
.wmeRcQuick:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcQuick:active{transform:translate3d(0,0,0);}
.wmeRcSplit{display:flex;gap:10px;flex-wrap:wrap;align-items:center;}
.wmeRcDT{display:flex;gap:10px;flex-wrap:wrap;align-items:center;}
.wmeRcDT > *{flex:1 1 160px;}
/* Custom date/time pickers */
.wmeRcPickerBtn{appearance:none;-webkit-appearance:none;cursor:pointer;
display:flex;align-items:center;justify-content:space-between;gap:10px;
padding:10px 12px;border-radius:14px;
border:1px solid rgba(255,255,255,.14);
background:rgba(255,255,255,.05);
color:#fff;font:800 13px/1.1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
transition:background .14s ease,border-color .14s ease,box-shadow .14s ease,transform .14s ease;}
.wmeRcPickerBtn:hover{background:rgba(255,255,255,.09);border-color:rgba(255,255,255,.22);transform:translate3d(0,-1px,0);}
.wmeRcPickerBtn:active{transform:translate3d(0,0,0);background:rgba(255,255,255,.11);}
.wmeRcPickerBtn .wmeRcPickerVal{opacity:.86;font-variant-numeric:tabular-nums;letter-spacing:.2px;font-weight:900;}
.wmeRcPickerPop{position:fixed;left:0;top:0;z-index:2147483647;
color:#fff;
min-width:280px;max-width:340px;
padding:12px;
background:linear-gradient(180deg,rgba(22,22,26,.46),rgba(12,12,14,.36));
border:none;border-radius:16px;
box-shadow:0 22px 60px rgba(0,0,0,.52);
backdrop-filter:blur(28px) saturate(160%);
-webkit-backdrop-filter:blur(28px) saturate(160%);
animation:wmeRcPickerIn .16s ease-out;
}
.wmeRcPickerPop{border:none !important;}
@keyframes wmeRcPickerIn{from{opacity:0;transform:translate3d(0,8px,0) scale(.985);}to{opacity:1;transform:translate3d(0,0,0) scale(1);}}
/* Calendar */
.wmeRcCalHdr{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px;}
.wmeRcCalTitle{flex:1;min-width:0;text-align:center;font-weight:950;letter-spacing:.2px;opacity:1;color:rgba(255,255,255,.92);}
.wmeRcCalNav{appearance:none;border:none;background:rgba(255,255,255,.06);
border:1px solid rgba(255,255,255,.14);
width:34px;height:34px;border-radius:12px;color:#fff;cursor:pointer;
font-weight:950;font-size:18px;line-height:1;
transition:background .14s ease,transform .14s ease;}
.wmeRcCalNav:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcCalDow{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;margin-bottom:8px;opacity:.70;font-size:11px;font-weight:800;text-align:center;}
.wmeRcCalGrid{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;}
.wmeRcCalCell{appearance:none;border:none;cursor:pointer;
height:34px;border-radius:12px;
background:rgba(255,255,255,.05);
border:1px solid rgba(255,255,255,.12);
color:#fff;font-weight:900;
transition:background .14s ease,transform .14s ease,border-color .14s ease,box-shadow .14s ease;}
.wmeRcCalCell.off{background:transparent;border-color:transparent;cursor:default;}
.wmeRcCalCell:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcCalCell.sel{background:rgba(var(--pinRGB,255,160,60),.18);border-color:rgba(var(--pinRGB,255,160,60),.38);box-shadow:0 0 0 4px rgba(var(--pinRGB,255,160,60),.10);}
/* Time picker */
.wmeRcTimeHdr{font-weight:950;letter-spacing:.2px;opacity:1;margin-bottom:10px;text-align:center;color:rgba(255,255,255,.92);}
.wmeRcTimeBody{display:grid;grid-template-columns:1fr 1fr;gap:10px;max-height:220px;overflow:auto;scrollbar-width:thin;}
.wmeRcTimeCol{display:flex;flex-direction:column;gap:6px;}
.wmeRcTimeCell{appearance:none;border:none;cursor:pointer;
padding:8px 10px;border-radius:12px;
background:rgba(255,255,255,.05);
border:1px solid rgba(255,255,255,.12);
color:#fff;font-weight:900;font-variant-numeric:tabular-nums;
transition:background .14s ease,transform .14s ease,border-color .14s ease;}
.wmeRcTimeCell:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcTimeCell.sel{background:rgba(255,160,60,.18);border-color:rgba(255,160,60,.38);}
.wmeRcTimeCustom{display:flex;gap:10px;align-items:center;margin-top:10px;}
.wmeRcTimeMinInp{flex:1;min-width:0;border-radius:14px;border:1px solid rgba(255,255,255,.14);
background:rgba(255,255,255,.05);color:#fff;padding:10px 12px;font-weight:900;outline:none;}
.wmeRcTimeOk{appearance:none;border:1px solid rgba(255,160,60,.30);background:rgba(255,160,60,.16);
color:#fff;border-radius:14px;padding:10px 14px;font-weight:950;cursor:pointer;transition:background .14s ease,transform .14s ease;}
.wmeRcTimeOk:hover{background:rgba(255,160,60,.22);transform:translate3d(0,-1px,0);}
/* Time picker (clock-based, no scrollbar) */
.wmeRcTimePop{min-width:300px;max-width:360px;}
.wmeRcPickerPop.wmeRcTimePop{padding-top:18px;}
.wmeRcTimeClockBox{display:flex;flex-direction:column;align-items:center;gap:12px;}
.wmeRcTimeClockLabel{font:900 18px/1.1 ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;
letter-spacing:.4px; color:rgba(255,255,255,.92);}
.wmeRcTimeClockInput{background:rgba(255,255,255,.04);border:none;outline:none;
text-align:center;width:112px;padding:7px 10px;border-radius:12px;
box-shadow:inset 0 0 0 1px rgba(255,255,255,.12);
color:rgba(255,255,255,.92);}
.wmeRcTimeClockInput:focus{box-shadow:inset 0 0 0 1px rgba(255,255,255,.26),0 0 0 4px rgba(255,255,255,.08);}
.wmeRcTimeClockHelp{opacity:.70;font-size:12px;text-align:center;margin-top:-4px;}
.wmeRcClockPick{width:160px;height:160px;}
.wmeRcClockPick .wmeRcClockTick{transform-origin:50% 78px;height:7px;}
.wmeRcClockPick::after{inset:10px;}
.wmeRcClockPick .wmeRcClockHand.min{height:62px;}
.wmeRcClockPick .wmeRcClockHand.hour{height:44px;}
.wmeRcClockPick .wmeRcClockHand{transition:none !important;}
.wmeRcClockPick .wmeRcClockNum{font-size:12px;}
.wmeRcTimeHandTabs{display:flex;gap:6px;align-items:center;justify-content:center;margin-top:2px;flex-wrap:wrap;}
.wmeRcTimeHandTab{cursor:pointer;padding:7px 10px;border-radius:999px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);
font-size:12px;opacity:.92;user-select:none;transition:.16s ease;}
.wmeRcTimeHandTab:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcTimeHandTab:active{transform:translate3d(0,0,0);}
.wmeRcTimeHandTab.on{border-color:rgba(255,255,255,.30);background:rgba(255,255,255,.14);box-shadow:0 0 0 4px rgba(255,255,255,.08);opacity:1;} .wmeRcTimeDoneRow{display:flex;gap:10px;justify-content:center;align-items:center;margin-top:12px;width:100%;}
.wmeRcTimeNowBtn{appearance:none;display:inline-flex;align-items:center;justify-content:center;line-height:1;
height:34px;padding:0 12px;border-radius:12px;font-weight:950;cursor:pointer;
border:1px solid rgba(255,160,60,.30) !important;
background:var(--wmeRcPinGrad, linear-gradient(180deg, rgba(255,200,120,.92), rgba(255,140,0,.92))) !important;
color:#fff !important;
box-shadow:0 12px 26px rgba(0,0,0,.35);
transition:filter .14s ease,transform .14s ease;}
.wmeRcTimeNowBtn:hover{filter:brightness(1.05);transform:translate3d(0,-1px,0);}
.wmeRcTimeNowBtn:active{transform:translate3d(0,0,0);filter:brightness(.98);}
.wmeRcTimeDoneBtn{appearance:none;display:inline-flex;align-items:center;justify-content:center;line-height:1;
height:34px;min-width:76px;padding:0 12px;
border:1px solid rgba(255,255,255,.16) !important;
background:rgba(255,255,255,.06) !important;
color:#fff !important;border-radius:12px;font-weight:950;cursor:pointer;
transition:background .14s ease,transform .14s ease,border-color .14s ease;}
.wmeRcTimeResetBtn{min-width:110px;opacity:.9;}
.wmeRcTimeDoneBtn:hover{background:rgba(255,255,255,.10) !important;transform:translate3d(0,-1px,0);}
.wmeRcTimeDoneBtn:active{transform:translate3d(0,0,0);background:rgba(255,255,255,.12) !important;}
/* Custom quick preset pop */
.wmeRcQuickPlus{font-weight:950;min-width:32px;justify-content:center;}
.wmeRcQuickPop{position:fixed;left:0;top:0;z-index:2147483647;
color:#fff;
min-width:260px;max-width:320px;
padding:12px;
background:linear-gradient(180deg,rgba(22,22,26,.46),rgba(12,12,14,.36));
border:1px solid rgba(255,255,255,.14);
border-radius:16px;
box-shadow:0 22px 60px rgba(0,0,0,.52);
backdrop-filter:blur(28px) saturate(160%);
-webkit-backdrop-filter:blur(28px) saturate(160%);
animation:wmeRcPickerIn .16s ease-out;}
.wmeRcQuickPopTitle{font-weight:950;letter-spacing:.2px;opacity:1;margin-bottom:10px;color:rgba(255,255,255,.92);}
.wmeRcQuickPopRow{display:flex;gap:10px;align-items:center;} .wmeRcQuickPopInput{flex:1;min-width:0;border-radius:12px;border:1px solid rgba(255,255,255,.14) !important;
background:rgba(255,255,255,.05) !important;color:#fff !important;
padding:0 12px;height:34px;line-height:34px;font-weight:900;outline:none;
font-variant-numeric:tabular-nums;}
.wmeRcQuickPopInput::placeholder{color:rgba(255,255,255,.55) !important;} .wmeRcQuickPopUnits{display:flex;gap:6px;align-items:center;height:34px;}
.wmeRcQuickPopUnit{appearance:none;display:inline-flex;align-items:center;justify-content:center;line-height:1;
height:34px;min-width:40px;
border:1px solid rgba(255,255,255,.16) !important;
background:rgba(255,255,255,.06) !important;
color:#fff !important;border-radius:12px;padding:0 12px;font-weight:950;cursor:pointer;
transition:background .14s ease,transform .14s ease,border-color .14s ease;}
.wmeRcQuickPopUnit:hover{background:rgba(255,255,255,.10) !important;transform:translate3d(0,-1px,0);}
.wmeRcQuickPopUnit:active{transform:translate3d(0,0,0);background:rgba(255,255,255,.12) !important;}
.wmeRcQuickPopUnit.sel{background:rgba(255,255,255,.14);border-color:rgba(255,255,255,.30);box-shadow:0 0 0 4px rgba(255,255,255,.08);} .wmeRcQuickPopSet{appearance:none;display:inline-flex;align-items:center;justify-content:center;line-height:1;
height:34px;padding:0 12px;border-radius:12px;font-weight:950;cursor:pointer;
border:1px solid rgba(255,160,60,.30) !important;
background:var(--wmeRcPinGrad, linear-gradient(180deg, rgba(255,200,120,.92), rgba(255,140,0,.92))) !important;
color:#fff !important;
box-shadow:0 12px 26px rgba(0,0,0,.35);
transition:filter .14s ease,transform .14s ease,border-color .14s ease;}
.wmeRcQuickPopSet:hover{filter:brightness(1.05);transform:translate3d(0,-1px,0);}
.wmeRcQuickPopSet:active{transform:translate3d(0,0,0);filter:brightness(.98);}
.wmeRcPinRow{
padding:6px 8px 6px 10px;
display:flex;align-items:center;justify-content:space-between;gap:8px;
cursor:pointer;transition:.16s ease;
position:relative;overflow:hidden;
border-radius:10px;
background:rgba(255,255,255,.08);
border:none;
}
.wmeRcPinRow>*{position:relative;z-index:3;}
/* colored left edge */
.wmeRcPinRow::before{
content:"";
position:absolute;left:0;top:0;bottom:0;
width:0;
transition:none;
background:rgb(var(--pinRGB,60,140,255));
border-top-left-radius:10px;
border-bottom-left-radius:10px;
pointer-events:none;
z-index:2;
}
/* inside glow from the left edge (pin color) */
.wmeRcPinRow::after{
content:"";
position:absolute;left:0;top:0;bottom:0;
width:200px;
background:linear-gradient(90deg,
rgba(var(--pinRGB,60,140,255),.26) 0%,
rgba(var(--pinRGB,60,140,255),.12) 35%,
rgba(var(--pinRGB,60,140,255),0) 72%);
pointer-events:none;
z-index:1;
}
.wmeRcPinRow:hover{background:rgba(255,255,255,.11);}
.wmeRcPinRow:hover .wmeRcPinLeft{transform:translateX(6px);}
.wmeRcPinRow:hover::before{width:5px;}
.wmeRcPinRow:active{background:rgba(255,255,255,.14);}
.wmeRcPinLeft{display:flex;flex-direction:column;gap:2px;min-width:0;transition:transform .18s cubic-bezier(.2,.8,.2,1);}
.wmeRcPinName{font-weight:800;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcPinSub{font-size:10.5px;opacity:.65;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.wmeRcPinBadName{
display:inline-flex;align-items:center;justify-content:center;
height:16px;padding:0 7px;border-radius:999px;
font-size:10.5px;font-weight:950;letter-spacing:.12px;
color:rgba(255,95,95,.95);
border:1px solid rgba(255,95,95,.28);
background:rgba(255,70,70,.10);
width:fit-content;
margin-bottom:4px;
}
.wmeRcPinNeedsRename .wmeRcPinName{color:rgba(255,210,210,.98);}
.wmeRcTooltip{position:fixed;z-index:2147483647;pointer-events:none;opacity:0;transform:translateY(-6px) scale(.98);transition:opacity .14s ease, transform .14s ease;max-width:min(520px,calc(100vw - 24px));
padding:8px 10px;border-radius:12px;background:rgba(16,16,18,.72);border:1px solid rgba(255,255,255,.14);backdrop-filter:blur(16px);
color:#fff;font:500 12px/1.15 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;letter-spacing:.2px;box-shadow:0 18px 34px rgba(0,0,0,.38);}
.wmeRcTooltip.show{opacity:1;transform:translateY(0) scale(1);}
.wmeRcPinBtns{display:flex;align-items:center;gap:6px;flex:0 0 auto;position:relative;justify-content:flex-end;width:34px;transition:width .22s cubic-bezier(.2,.9,.2,1);overflow:visible;}
.wmeRcPinRow:hover .wmeRcPinBtns{width:160px;}
.wmeRcPinDrag{width:26px;height:26px;border-radius:10px;border:1px solid rgba(255,255,255,.14);
background:rgba(255,255,255,.04);display:flex;align-items:center;justify-content:center;cursor:grab;flex:0 0 auto;
opacity:.75;transition:opacity .14s ease,background .14s ease,border-color .14s ease;user-select:none;touch-action:none;}
.wmeRcPinRow:hover .wmeRcPinDrag{opacity:1;background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.18);}
.wmeRcPinDrag:active{cursor:grabbing;background:rgba(255,255,255,.08);}
.wmeRcPinDrag .wmeRcI{width:16px;height:16px;display:flex;align-items:center;justify-content:center;}
.wmeRcPinPlaceholder{height:36px;border-radius:12px;border:1px dashed rgba(255,255,255,.18);
background:rgba(255,255,255,.03);margin:2px 0;}
.wmeRcPinRow.reorderFloat{position:fixed !important;left:0;top:0;z-index:2147483646;
box-shadow:0 24px 70px rgba(0,0,0,.55);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
cursor:grabbing;}
.wmeRcPinGhost{z-index:2147483646;box-shadow:0 24px 70px rgba(0,0,0,.55);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);
border-radius:12px;border:1px solid rgba(255,255,255,.16);}
.wmeRcNoSelect, .wmeRcNoSelect *{user-select:none !important;-webkit-user-select:none !important;}
.wmeRcLimitMsg{display:none;margin-top:8px;font-size:12px;font-weight:800;letter-spacing:.1px;
color:rgba(255,95,95,.95);background:rgba(255,70,70,.10);border:1px solid rgba(255,70,70,.20);
border-radius:12px;padding:7px 10px;}
.wmeRcLenHost{position:relative;flex:1 1 auto;min-width:0;display:flex;align-items:center;}
.wmeRcLenHost>.wmeRcInput{flex:1 1 auto;min-width:0;}
.wmeRcInput.wmeRcHasCount{padding-right:72px !important;}
.wmeRcLenCountIn{position:absolute;right:10px;top:50%;transform:translate3d(0,-50%,0);font-size:11px;font-weight:900;opacity:.62;pointer-events:none;font-variant-numeric:tabular-nums;letter-spacing:.2px;}
.wmeRcLenMeta{display:flex;align-items:center;justify-content:flex-end;margin-top:6px;font-size:11px;font-weight:900;opacity:.72;}
.wmeRcLenCount{font-variant-numeric:tabular-nums;letter-spacing:.2px;}
.wmeRcLenWrap .wmeRcLimitMsg{margin-top:6px;}
.wmeRcPinBtn{cursor:pointer;user-select:none;width:28px;height:28px;border-radius:10px;border:1px solid rgba(255,255,255,.16);background:rgba(255,255,255,.06);display:flex;align-items:center;justify-content:center;transition:.16s ease;position:relative;}
.wmeRcPinBtn:hover{background:rgba(255,255,255,.10);transform:translate3d(0,-1px,0);}
.wmeRcPinBtn:active{background:rgba(255,255,255,.14);transform:translate3d(0,0,0);}
.wmeRcPinBtnEye:hover{transform:none;}
.wmeRcPinBtnEye:active{transform:none;}
.wmeRcPinBtn .wmeRcI{width:16px;height:16px;display:flex;align-items:center;justify-content:center;}
.wmeRcPinBtn svg{width:16px;height:16px;}
.wmeRcPinBtnHoverOnly{
z-index:2;
opacity:0;
pointer-events:none;
position:absolute;
top:0;
transform:translate3d(10px,0,0) scale(.98);
transition:
opacity .16s ease,
transform .22s cubic-bezier(.2,.9,.2,1),
background .16s ease,
box-shadow .16s ease,
border-color .16s ease;
}
.wmeRcPinRow:hover .wmeRcPinBtnHoverOnly{
opacity:1;
pointer-events:auto;
transform:translate3d(0,0,0) scale(1);
}
.wmeRcPinBtnEye{right:102px;}
.wmeRcPinBtnEdit{right:68px;}
.wmeRcPinBtnTrash{right:34px;}
.wmeRcPinBtnBell{
position:absolute;
right:0;
top:0;
z-index:3;
will-change:transform;
}
.wmeRcPinRow:hover .wmeRcPinBtnHoverOnly.wmeRcPinBtnTrash{transition-delay:65ms;}
.wmeRcPinBtnEye.isHidden::after{content:"";position:absolute;left:6px;right:6px;top:50%;height:2px;background:rgba(255,255,255,.92);transform:translateY(-50%) rotate(-32deg);border-radius:99px;box-shadow:0 2px 10px rgba(0,0,0,.35);opacity:.95;pointer-events:none;}
.wmeRcPinRow.dragging{opacity:.55;}
.wmeRcPinRow.dropBefore{box-shadow:inset 0 2px 0 rgba(255,255,255,.32);}
.wmeRcPinRow.dropAfter{box-shadow:inset 0 -2px 0 rgba(255,255,255,.32);}
.wmeRcPinDot{width:8px;height:8px;border-radius:99px;background:rgba(255,188,75,.9);box-shadow:0 0 0 4px rgba(255,188,75,.12);margin-left:6px;flex:0 0 auto;}
.wmeRcPinDot.bell{width:20px;height:20px;border-radius:10px;border:1px solid rgba(255,188,75,.35);background:rgba(255,188,75,.12);box-shadow:0 0 0 4px rgba(255,188,75,.08);display:flex;align-items:center;justify-content:center;margin-left:6px;animation:wmeRcBellPulse 1.2s ease-in-out infinite;}
.wmeRcSwitch{display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none;}
.wmeRcSwitch input{position:absolute;opacity:0;pointer-events:none;}
.wmeRcSwitchTrack{width:38px;height:22px;border-radius:999px;background:rgba(255,255,255,.10);border:1px solid rgba(255,255,255,.16);display:inline-flex;align-items:center;padding:2px;transition:.18s ease;box-shadow:inset 0 0 0 999px rgba(0,0,0,.05);}
.wmeRcSwitchThumb{width:18px;height:18px;border-radius:999px;background:rgba(255,255,255,.86);transform:translate3d(0,0,0);transition:.18s ease;box-shadow:0 6px 16px rgba(0,0,0,.25);}
.wmeRcInlineToggle{display:flex;align-items:center;gap:10px;cursor:pointer;user-select:none;}
.wmeRcInlineToggle input{position:absolute;opacity:0;pointer-events:none;}
.wmeRcInlineToggle input:checked + .wmeRcSwitchTrack{background:rgba(255,160,60,.28);border-color:rgba(255,160,60,.55);box-shadow:inset 0 0 0 999px rgba(255,160,60,.08);}
.wmeRcInlineToggle input:checked + .wmeRcSwitchTrack .wmeRcSwitchThumb{transform:translate3d(16px,0,0);}
.wmeRcSwitchLabel{color:rgba(255,255,255,.90);font-size:12.5px;white-space:nowrap;}
.wmeRcSwitch input:checked + .wmeRcSwitchTrack{background:rgba(255,160,60,.20);border-color:rgba(255,160,60,.32);}
.wmeRcSwitch input:checked + .wmeRcSwitchTrack .wmeRcSwitchThumb{transform:translate3d(16px,0,0);}
.wmeRcPanel, .wmeRcPanel *{-webkit-user-select:none !important;user-select:none !important;}
`;
document.documentElement.appendChild(s);
}
// Inline toggle click policy
// - Clicking the row/label should NOT toggle.
// - Clicking the actual switch (track/thumb) SHOULD toggle.
// We implement this via delegated capture click handling and manual toggling,
// to avoid browser/label retargeting edge-cases.
(function installInlineToggleClickPolicy(){
if (UW.__wmeRcInlineToggleClickPolicyInstalled) return;
UW.__wmeRcInlineToggleClickPolicyInstalled = true;
const isOnSwitch = (e) => {
const t = e && e.target;
if (t && t.closest) {
if (t.closest(".wmeRcSwitchTrack") || t.closest(".wmeRcSwitchThumb")) return true;
}
const path = (e && typeof e.composedPath === "function") ? e.composedPath() : null;
if (path && path.length) {
for (const n of path) {
if (!n || !n.classList) continue;
if (n.classList.contains("wmeRcSwitchTrack") || n.classList.contains("wmeRcSwitchThumb")) return true;
}
}
return false;
};
const handler = (e) => {
try {
const t = e && e.target;
const lbl = t && t.closest && t.closest("label.wmeRcInlineToggle");
if (!lbl) return;
const input = lbl.querySelector('input[type="checkbox"]');
if (!input) return;
// Only allow toggle when clicking the actual switch.
if (isOnSwitch(e)) {
// Prevent the label default toggle (and double-toggles).
e.preventDefault();
e.stopPropagation();
input.checked = !input.checked;
try {
input.dispatchEvent(new Event("change", { bubbles: true }));
} catch {
try { const ev = document.createEvent("Event"); ev.initEvent("change", true, false); input.dispatchEvent(ev); } catch {}
}
return;
}
// Clicked elsewhere on the row/label -> do nothing (and prevent default label toggle).
e.preventDefault();
e.stopPropagation();
} catch {}
};
// Capture so we beat the label default behavior.
try { document.addEventListener("click", handler, true); } catch {}
})();
function playReminderSoundOnce(soundId, opts = {}) {
const nowMs = Date.now();
const force = opts && opts.force === true;
if (!force && (nowMs - lastBellAt < 700)) return;
lastBellAt = nowMs;
try {
const AudioCtx = window.AudioContext || window.webkitAudioContext;
if (!AudioCtx) return;
const ctx = playReminderSoundOnce._ctx || (playReminderSoundOnce._ctx = new AudioCtx());
try { if (ctx.state === "suspended") ctx.resume(); } catch {}
const t0 = ctx.currentTime;
const out = ctx.createGain();
out.gain.setValueAtTime(0.0001, t0);
out.connect(ctx.destination);
const id = (typeof soundId === "string" ? soundId : "") || getReminderSoundId();
if (id === "mute") return;
const env = (attack, peak, decay) => {
out.gain.cancelScheduledValues(t0);
out.gain.setValueAtTime(0.0001, t0);
out.gain.exponentialRampToValueAtTime(Math.max(0.0002, peak), t0 + attack);
out.gain.exponentialRampToValueAtTime(0.0001, t0 + attack + decay);
};
const osc = (type, f0, f1, dur) => {
const o = ctx.createOscillator();
o.type = type;
o.frequency.setValueAtTime(f0, t0);
if (f1 && f1 > 0) {
try { o.frequency.exponentialRampToValueAtTime(f1, t0 + dur); } catch {}
}
o.connect(out);
o.start(t0);
o.stop(t0 + dur + 0.02);
return o;
};
if (id === "softBell") {
env(0.02, 0.22, 1.1);
osc("triangle", 784, 392, 1.05);
osc("sine", 1175, 587, 0.9);
return;
}
if (id === "chime") {
env(0.01, 0.24, 0.75);
osc("sine", 1046, 880, 0.65);
osc("sine", 1568, 1320, 0.55);
return;
}
if (id === "doubleDing") {
env(0.008, 0.26, 0.28);
osc("sine", 988, 740, 0.25);
const t1 = t0 + 0.32;
const out2 = ctx.createGain();
out2.gain.setValueAtTime(0.0001, t1);
out2.gain.exponentialRampToValueAtTime(0.22, t1 + 0.01);
out2.gain.exponentialRampToValueAtTime(0.0001, t1 + 0.32);
out2.connect(ctx.destination);
const o2 = ctx.createOscillator();
o2.type = "sine";
o2.frequency.setValueAtTime(880, t1);
o2.frequency.exponentialRampToValueAtTime(660, t1 + 0.28);
o2.connect(out2);
o2.start(t1);
o2.stop(t1 + 0.34);
return;
}
if (id === "digital") {
env(0.002, 0.18, 0.22);
osc("square", 880, 880, 0.18);
const t1 = t0 + 0.22;
const out2 = ctx.createGain();
out2.gain.setValueAtTime(0.0001, t1);
out2.gain.exponentialRampToValueAtTime(0.16, t1 + 0.002);
out2.gain.exponentialRampToValueAtTime(0.0001, t1 + 0.2);
out2.connect(ctx.destination);
const o2 = ctx.createOscillator();
o2.type = "square";
o2.frequency.setValueAtTime(660, t1);
o2.connect(out2);
o2.start(t1);
o2.stop(t1 + 0.22);
return;
}
if (id === "alarm") {
env(0.01, 0.22, 0.55);
osc("sawtooth", 660, 440, 0.5);
osc("sine", 990, 660, 0.45);
return;
}
if (id === "alarmFast") {
env(0.004, 0.18, 0.22);
osc("square", 880, 880, 0.12);
const t1 = t0 + 0.16;
const g1 = ctx.createGain();
g1.gain.setValueAtTime(0.0001, t1);
g1.gain.exponentialRampToValueAtTime(0.16, t1 + 0.003);
g1.gain.exponentialRampToValueAtTime(0.0001, t1 + 0.18);
g1.connect(ctx.destination);
const o1 = ctx.createOscillator();
o1.type = "square";
o1.frequency.setValueAtTime(880, t1);
o1.connect(g1);
o1.start(t1);
o1.stop(t1 + 0.2);
const t2 = t0 + 0.34;
const g2 = ctx.createGain();
g2.gain.setValueAtTime(0.0001, t2);
g2.gain.exponentialRampToValueAtTime(0.16, t2 + 0.003);
g2.gain.exponentialRampToValueAtTime(0.0001, t2 + 0.18);
g2.connect(ctx.destination);
const o2 = ctx.createOscillator();
o2.type = "square";
o2.frequency.setValueAtTime(740, t2);
o2.connect(g2);
o2.start(t2);
o2.stop(t2 + 0.2);
return;
}
if (id === "alarmPulse") {
env(0.01, 0.20, 0.65);
const o = ctx.createOscillator();
o.type = "sawtooth";
o.frequency.setValueAtTime(520, t0);
o.frequency.linearRampToValueAtTime(780, t0 + 0.28);
o.frequency.linearRampToValueAtTime(520, t0 + 0.56);
o.connect(out);
o.start(t0);
o.stop(t0 + 0.60);
return;
}
if (id === "buzzer") {
env(0.003, 0.20, 0.35);
osc("square", 220, 220, 0.28);
osc("square", 330, 330, 0.22);
return;
}
if (id === "radar") {
env(0.004, 0.20, 0.55);
osc("sine", 1568, 1046, 0.45);
osc("triangle", 784, 523, 0.35);
return;
}
if (id === "wood") {
env(0.002, 0.18, 0.18);
osc("triangle", 330, 220, 0.14);
osc("triangle", 660, 440, 0.12);
return;
}
if (id === "glass") {
env(0.008, 0.22, 0.95);
osc("sine", 1760, 880, 0.9);
osc("sine", 2640, 1320, 0.75);
return;
}
if (id === "church") {
env(0.01, 0.26, 1.55);
osc("sine", 659, 330, 1.5);
osc("sine", 988, 494, 1.2);
osc("triangle", 1319, 660, 1.0);
return;
}
if (id === "retro") {
env(0.002, 0.16, 0.18);
osc("square", 880, 880, 0.10);
const t1 = t0 + 0.13;
const g1 = ctx.createGain();
g1.gain.setValueAtTime(0.0001, t1);
g1.gain.exponentialRampToValueAtTime(0.14, t1 + 0.002);
g1.gain.exponentialRampToValueAtTime(0.0001, t1 + 0.12);
g1.connect(ctx.destination);
const o1 = ctx.createOscillator();
o1.type = "square";
o1.frequency.setValueAtTime(660, t1);
o1.connect(g1);
o1.start(t1);
o1.stop(t1 + 0.14);
const t2 = t0 + 0.28;
const g2 = ctx.createGain();
g2.gain.setValueAtTime(0.0001, t2);
g2.gain.exponentialRampToValueAtTime(0.13, t2 + 0.002);
g2.gain.exponentialRampToValueAtTime(0.0001, t2 + 0.12);
g2.connect(ctx.destination);
const o2 = ctx.createOscillator();
o2.type = "square";
o2.frequency.setValueAtTime(784, t2);
o2.connect(g2);
o2.start(t2);
o2.stop(t2 + 0.14);
return;
}
if (id === "siren") {
env(0.01, 0.18, 0.75);
const o = ctx.createOscillator();
o.type = "sine";
o.frequency.setValueAtTime(520, t0);
o.frequency.linearRampToValueAtTime(740, t0 + 0.35);
o.frequency.linearRampToValueAtTime(520, t0 + 0.7);
o.connect(out);
o.start(t0);
o.stop(t0 + 0.72);
return;
}
if (id === "gong") {
env(0.01, 0.22, 1.8);
osc("sine", 196, 98, 1.7);
osc("triangle", 294, 147, 1.5);
osc("sine", 392, 196, 1.2);
return;
}
env(0.015, 0.28, 1.25);
osc("sine", 880, 440, 1.2);
osc("sine", 1320, 660, 1.0);
} catch {}
}
function startBellLoop() {
try {
if (bellLoopId) return;
playReminderSoundOnce(getReminderSoundId());
bellLoopId = window.setInterval(() => {
try { playReminderSoundOnce(getReminderSoundId()); } catch {}
}, 1600);
} catch {}
}
function stopBellLoop() {
try {
if (bellLoopId) {
window.clearInterval(bellLoopId);
bellLoopId = null;
}
} catch {}
}
function startPinsMapSync() {
if (pinsMapSyncStarted) return;
const ol = UW?.OpenLayers;
const map = getOlMapBestEffort();
if (!ol || !map || !map.events?.register) return;
pinsMapSyncStarted = true;
let scheduled = 0;
const scheduleRender = () => {
if (scheduled) return;
scheduled = window.setTimeout(() => {
scheduled = 0;
try { renderPinsMarkers(); } catch {}
}, 120);
};
try { map.events.register("moveend", map, scheduleRender); } catch {}
try { map.events.register("zoomend", map, scheduleRender); } catch {}
try { map.events.register("changelayer", map, scheduleRender); } catch {}
try {
map.events.register("mousemove", map, (evt) => {
try {
if (!startPinsMapSync._cursorRaf) startPinsMapSync._cursorRaf = 0;
if (!startPinsMapSync._cursorHit) startPinsMapSync._cursorHit = false;
if (!startPinsMapSync._cursorLast) startPinsMapSync._cursorLast = null;
startPinsMapSync._cursorHit = (function() {
const layer = ensurePinsOlLayer();
if (layer && typeof layer.getMarkerFromEvent === "function") {
const mk = layer.getMarkerFromEvent(evt);
return !!(mk && (mk.pinId || mk.clusterId));
}
const t = evt?.target || evt?.srcElement;
const el = t && t.closest ? t.closest(".wmeRcPinMarker, .wmeRcPinCluster, .wmeRcOlMarker") : null;
return !!el;
})();
if (startPinsMapSync._cursorRaf) return;
startPinsMapSync._cursorRaf = window.requestAnimationFrame(() => {
startPinsMapSync._cursorRaf = 0;
try {
const div = map.div || map.viewPortDiv || map.layerContainerDiv;
if (!div) return;
const hit = !!startPinsMapSync._cursorHit;
if (startPinsMapSync._cursorLast === hit) return;
startPinsMapSync._cursorLast = hit;
div.style.cursor = hit ? "pointer" : "";
} catch {}
});
} catch {}
});} catch {}
}
let _wmeRcPinHitPill = null;
let _wmeRcPinHitPillTimer = null;
function showPinHitPill(name) {
try {
ensureCSS();
const label = String(name || "").trim() || "Pinned place";
if (!_wmeRcPinHitPill || !_wmeRcPinHitPill.isConnected) {
const el = document.createElement("div");
el.className = "wmeRcPinHitPill";
const t = document.createElement("div");
t.className = "wmeRcPinHitText";
t.textContent = label;
const x = document.createElement("div");
x.className = "wmeRcPinHitClose";
x.title = "Close";
x.textContent = "×";
x.addEventListener("click", (ev) => {
try { ev.stopPropagation(); ev.preventDefault(); } catch {}
try { el.classList.remove("show"); } catch {}
try { if (_wmeRcPinHitPillTimer) { clearTimeout(_wmeRcPinHitPillTimer); _wmeRcPinHitPillTimer = null; } } catch {}
});
el.appendChild(t);
el.appendChild(x);
(document.body || document.documentElement).appendChild(el);
_wmeRcPinHitPill = el;
}
const txtEl = _wmeRcPinHitPill.querySelector(".wmeRcPinHitText");
if (txtEl) txtEl.textContent = label;
try { if (_wmeRcPinHitPillTimer) { clearTimeout(_wmeRcPinHitPillTimer); _wmeRcPinHitPillTimer = null; } } catch {}
_wmeRcPinHitPill.classList.add("show");
_wmeRcPinHitPillTimer = setTimeout(() => {
try { _wmeRcPinHitPill && _wmeRcPinHitPill.classList.remove("show"); } catch {}
try { _wmeRcPinHitPillTimer = null; } catch {}
}, 2200);
} catch {}
}
function toast(msg) {
ensureCSS();
const el = document.createElement("div");
el.className = "wmeRcToast";
el.textContent = msg;
(document.body || document.documentElement).appendChild(el);
requestAnimationFrame(() => el.classList.add("show"));
setTimeout(() => {
el.classList.remove("show");
setTimeout(() => el.remove(), 250);
}, 1400);
}
function normalizePinColor(c) {
try {
if (typeof c !== "string") return "#ff8a00";
let s = c.trim();
if (!s) return "#ff8a00";
if (s[0] !== "#") s = "#" + s;
if (!/^#[0-9a-fA-F]{6}$/.test(s)) return "#ff8a00";
return s.toLowerCase();
} catch {
return "#ff8a00";
}
}
function hexToRgb(hex) {
const h = normalizePinColor(hex).slice(1);
const r = parseInt(h.slice(0,2), 16);
const g = parseInt(h.slice(2,4), 16);
const b = parseInt(h.slice(4,6), 16);
return { r, g, b };
}
function mixRgb(a, b, t) {
return {
r: Math.round(a.r + (b.r - a.r) * t),
g: Math.round(a.g + (b.g - a.g) * t),
b: Math.round(a.b + (b.b - a.b) * t),
};
}
function rgbaStr(rgb, a) {
return `rgba(${rgb.r},${rgb.g},${rgb.b},${a})`;
}
function applyPinMarkerColors(el, hex) {
const baseHex = normalizePinColor(hex);
const base = hexToRgb(baseHex);
const light = mixRgb(base, { r:255, g:255, b:255 }, 0.35);
el.style.setProperty("--wmeRcPinGrad", `linear-gradient(180deg, ${rgbaStr(light, .95)}, ${rgbaStr(base, .95)})`);
el.style.setProperty("--wmeRcPinTip", rgbaStr(base, .95));
el.style.setProperty("--wmeRcPinGlow", rgbaStr(base, .34));
el.style.setProperty("--wmeRcPinGlow2", rgbaStr(base, .22));
}
function upsertMapPinLabel(hostEl, pin, show) {
try {
if (!hostEl) return;
const name = (pin && typeof pin.name === "string") ? pin.name.trim() : "";
let lbl = null;
try { lbl = hostEl.querySelector(":scope > .wmeRcPinLabel"); } catch { lbl = hostEl.querySelector(".wmeRcPinLabel"); }
if (!show || !name) {
if (lbl) lbl.style.display = "none";
return;
}
if (!lbl) {
lbl = document.createElement("div");
lbl.className = "wmeRcPinLabel";
hostEl.appendChild(lbl);
}
lbl.style.display = "block";
lbl.textContent = name;
lbl.title = name;
try {
const c = normalizePinColor((pin && pin.color) || "#ff8a00");
const rgb = hexToRgb(c);
if (rgb) {
lbl.style.setProperty("--pinRGB", `${rgb.r},${rgb.g},${rgb.b}`);
const luma = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255;
const fg = (luma > 0.86) ? "#111" : "#fff";
const shadow = (fg === "#111")
? "0 6px 18px rgba(255,255,255,.18)"
: "0 6px 18px rgba(0,0,0,.35)";
lbl.style.setProperty("--wmeRcPinLabelFg", fg);
lbl.style.setProperty("--wmeRcPinLabelShadow", shadow);
}
} catch {}
} catch {}
}
function loadPins() {
try {
if (_pinsMem && Array.isArray(_pinsMem)) return _pinsMem;
const raw = localStorage.getItem(PIN_KEY);
_pinsLastJson = (typeof raw === "string") ? raw : _pinsLastJson;
const arr = JSON.parse(raw || "[]");
if (!Array.isArray(arr)) { _pinsMem = []; return _pinsMem; }
_pinsMem = arr
.filter((p) => p && Number.isFinite(Number(p.lon)) && Number.isFinite(Number(p.lat)))
.map((p) => {
const reminderAt = Number.isFinite(Number(p.reminderAt)) ? Number(p.reminderAt) : null;
const reminderDone = !!p.reminderDone;
const reminderFiredAt = Number.isFinite(Number(p.reminderFiredAt)) ? Number(p.reminderFiredAt) : null;
const reminderType = (p.reminderType === "IN" || p.reminderType === "AT") ? p.reminderType : null;
const reminderUnit = (p.reminderUnit === "MINUTES" || p.reminderUnit === "HOURS") ? p.reminderUnit : null;
const reminderValue = Number.isFinite(Number(p.reminderValue)) ? Number(p.reminderValue) : null;
const reminderNote = (typeof p.reminderNote === "string") ? p.reminderNote : "";
return ({
id: String(p.id || ""),
name: String(p.name || "Pinned place"),
lon: Number(p.lon),
lat: Number(p.lat),
zoom: Number.isFinite(Number(p.zoom)) ? Number(p.zoom) : null,
createdAt: Number.isFinite(Number(p.createdAt)) ? Number(p.createdAt) : Date.now(),
color: normalizePinColor(p.color),
groupId: normalizeGroupId(p.groupId),
hideOnMap: p.hideOnMap === true,
reminderAt,
reminderDone: reminderAt ? reminderDone : false,
reminderFiredAt: reminderAt ? reminderFiredAt : null,
reminderType,
reminderUnit,
reminderValue,
reminderNote,
});
})
.filter((p) => p.id);
return _pinsMem;
} catch {
_pinsMem = _pinsMem && Array.isArray(_pinsMem) ? _pinsMem : [];
return _pinsMem;
}
}
function savePins(pins) {
try {
_pinsMem = Array.isArray(pins) ? pins : [];
_schedulePinsSaveNow(_pinsMem);
} catch {}
}
function getCurrentZoomBestEffort() {
try {
const z = sdk?.Map?.getZoom?.();
if (Number.isFinite(z)) return Number(z);
} catch {}
try {
const z = UW?.W?.map?.getZoom?.();
if (Number.isFinite(z)) return Number(z);
} catch {}
return null;
}
function ensurePinsPanel() {
ensureCSS();
const mount = () => {
const mapEl =
document.querySelector("#map") ||
document.querySelector("#WazeMap") ||
document.querySelector(".olMap") ||
document.querySelector(".wme-map") ||
null;
if (!mapEl) return false;
try {
const cs = getComputedStyle(mapEl);
if (cs.position === "static") mapEl.style.position = "relative";
} catch {}
if (!pinsPanelEl || !document.contains(pinsPanelEl)) {
const el = document.createElement("div");
el.className = "wmeRcPins hidden";
try {
el.addEventListener("wheel", (ev) => { try { ev.stopPropagation(); } catch {} }, { passive: true, capture: true });
} catch {}
try {
const pos = JSON.parse(localStorage.getItem(PIN_POS_KEY) || "null");
if (pos && Number.isFinite(pos.left) && Number.isFinite(pos.top)) {
el.style.left = `${Math.max(0, pos.left)}px`;
el.style.top = `${Math.max(0, pos.top)}px`;
}
} catch {}
try {
const sz = loadPinsPanelSize();
if (sz && Number.isFinite(sz.h)) {
_pinsPanelLastAutoAt = _pinsNow();
_pinsPanelSuppressROUntil = _pinsPanelLastAutoAt + 900;
el.style.height = `${Math.round(sz.h)}px`;
if (sz.manual) el.dataset.manualHeight = "1";
}
} catch {}
try {
if (loadPinsPanelCollapsed()) {
el.classList.add("collapsed");
el.dataset.collapsed = "1";
try { el.style.resize = "none"; } catch {}
}
} catch {}
const stop = (ev) => { try { ev.stopPropagation(); } catch {} };
el.addEventListener("mousedown", stop, false);
el.addEventListener("click", stop, false);
el.addEventListener("dblclick", stop, false);
el.addEventListener("contextmenu", stop, false);
el.addEventListener("touchstart", stop, false);
el.addEventListener("pointerdown", (ev) => {
try {
const hdr = ev.target && ev.target.closest && ev.target.closest(".wmeRcPinsHdr");
if (!hdr) return;
if (ev.target && ev.target.closest && (ev.target.closest(".wmeRcSelect") || ev.target.closest(".wmeRcPinsHdrBtn"))) return;
if (ev.button != null && ev.button !== 0) return; // left click only
ev.preventDefault();
stop(ev);
const rect = el.getBoundingClientRect();
const mapRect = mapEl.getBoundingClientRect();
pinsDrag.active = true;
pinsDrag.pointerId = ev.pointerId;
pinsDrag.startX = ev.clientX;
pinsDrag.startY = ev.clientY;
pinsDrag.baseLeft = rect.left - mapRect.left;
pinsDrag.baseTop = rect.top - mapRect.top;
try { el.setPointerCapture(ev.pointerId); } catch {}
document.addEventListener("pointermove", onPinsDragMove, true);
document.addEventListener("pointerup", onPinsDragUp, true);
document.addEventListener("pointercancel", onPinsDragUp, true);
} catch {}
}, false);
mapEl.appendChild(el);
pinsPanelEl = el;
try { _ensurePinsPanelDockObserver(); } catch {}
try {
if (!_pinsPanelRO && typeof ResizeObserver !== "undefined") {
_pinsPanelRO = new ResizeObserver(() => {
try {
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return;
if (pinsPanelEl.classList.contains("collapsed") || (pinsPanelEl.dataset && pinsPanelEl.dataset.collapsed === "1")) return;
if (_pinsPanelAutoSizing) return;
if (_pinsNow() < _pinsPanelSuppressROUntil) return;
const h = Math.round(pinsPanelEl.getBoundingClientRect().height || 0);
if (!Number.isFinite(h) || h <= 0) return;
savePinsPanelSize({ h, manual: true, u: 1 });
pinsPanelEl.dataset.manualHeight = "1";
} catch {}
});
_pinsPanelRO.observe(el);
}
} catch {}
}
renderPinsPanel();
try { applyPinsPanelMinimizedOnLoad(); } catch {}
try { _syncPinsBubbleVisibility(); } catch {}
try { updatePinsPanelHeightBounds(); } catch {}
if (!_pinsPanelResizeBound) {
_pinsPanelResizeBound = true;
try { window.addEventListener("resize", () => { try { updatePinsPanelHeightBounds(); } catch {} }, true); } catch {}
}
startReminderLoop();
try { scheduleAllReminderTimers(); } catch {}
return true;
};
if (mount()) return;
const t = setInterval(() => { if (mount()) clearInterval(t); }, 650);
setTimeout(() => clearInterval(t), 16000);
}
function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
function loadPinsPanelSize() {
try {
const raw = localStorage.getItem(PIN_PANEL_SIZE_KEY);
const obj = JSON.parse(raw || "null");
if (!obj || typeof obj !== "object") return null;
const h = Number(obj.h);
const manualRaw = !!obj.manual;
const u = Number(obj.u || 0);
const manual = manualRaw && (u === 1);
if (!Number.isFinite(h) || h <= 0) return null;
return { h, manual };
} catch { return null; }
}
function savePinsPanelSize(o) {
try { localStorage.setItem(PIN_PANEL_SIZE_KEY, JSON.stringify(o || {})); } catch {}
}
function loadPinsPanelCollapsed() {
try { return localStorage.getItem(PIN_PANEL_COLLAPSED_KEY) === "1"; } catch { return false; }
}
function loadPinsPanelMinimizedRaw() {
try {
const v = localStorage.getItem(PIN_PANEL_MINIMIZED_KEY);
if (v === null) return null;
return v === "1";
} catch { return null; }
}
function savePinsPanelMinimized(v) {
try { localStorage.setItem(PIN_PANEL_MINIMIZED_KEY, v ? "1" : "0"); } catch {}
}
function applyPinsPanelMinimizedOnLoad() {
try {
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return;
const mode = getPinsMinimizeMode();
const minimizedRaw = loadPinsPanelMinimizedRaw();
const minimized = (minimizedRaw === true);
if (mode !== "bubble") {
// In panel mode we never show the bubble.
try { _hidePinsBubble(true); } catch {}
// If a previous bubble-minimize left the panel hidden, unhide it.
try {
if (pinsPanelEl.style && pinsPanelEl.style.display === "none") pinsPanelEl.style.display = "block";
} catch {}
try { savePinsPanelMinimized(false); } catch {}
return;
}
// Bubble mode
if (minimized) {
const mapEl = pinsPanelEl.parentElement || _getMapElForPins();
try { _ensurePinsBubble(mapEl); } catch {}
try { pinsPanelEl.style.display = "none"; } catch {}
try { _showPinsBubble(false); } catch {}
} else {
// Not minimized: show panel, hide bubble
try {
if (pinsPanelEl.style && pinsPanelEl.style.display === "none") pinsPanelEl.style.display = "block";
} catch {}
try { _hidePinsBubble(true); } catch {}
}
} catch {}
}
function getPinsMinimizeMode() {
try {
const v = String(localStorage.getItem(PIN_MINIMIZE_MODE_KEY) || "");
return (v === "panel" || v === "bubble") ? v : "bubble";
} catch { return "bubble"; }
}
function setPinsMinimizeMode(mode) {
try {
const v = (mode === "panel") ? "panel" : "bubble";
localStorage.setItem(PIN_MINIMIZE_MODE_KEY, v);
} catch {}
}
function loadPinsBubblePos() {
try {
const raw = localStorage.getItem(PIN_BUBBLE_POS_KEY);
const obj = JSON.parse(raw || "null");
if (!obj || typeof obj !== "object") return null;
const left = Number(obj.left);
const top = Number(obj.top);
if (!Number.isFinite(left) || !Number.isFinite(top)) return null;
return { left, top };
} catch { return null; }
}
function savePinsBubblePos(pos) {
try {
if (!pos || !Number.isFinite(Number(pos.left)) || !Number.isFinite(Number(pos.top))) return;
localStorage.setItem(PIN_BUBBLE_POS_KEY, JSON.stringify({ left: Math.round(pos.left), top: Math.round(pos.top) }));
} catch {}
}
// Resolve the WME map container used for pins UI and bubble.
function _getMapElForPins() {
try { return getMapContainerEl(); } catch { return null; }
}
function _isPinsPanelMinimizedNow() {
try {
// IMPORTANT: "collapsed" is a UI state (header-only) and should NOT be treated as
// "minimized-to-bubble". The bubble should only show when the panel is actually
// minimized (saved flag) or hidden.
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return true;
// If we explicitly saved "minimized", treat it as minimized even during animations.
try { if (loadPinsPanelMinimizedRaw() === true) return true; } catch {}
// Fallback: if the element is hidden, it's minimized.
if (pinsPanelEl.style && pinsPanelEl.style.display === "none") return true;
return false;
} catch { return false; }
}
function _syncPinsBubbleVisibility() {
try {
const mode = getPinsMinimizeMode();
if (mode !== "bubble") {
_hidePinsBubble(true);
return;
}
// During open animation we force-hide to prevent overlap (but never hide while minimized-to-bubble).
try {
if (_pinsBubbleForceHideUntil && _pinsNow() < _pinsBubbleForceHideUntil) {
if (!_isPinsPanelMinimizedNow()) { _hidePinsBubble(true); return; }
}
} catch {}
if (_isPinsPanelMinimizedNow()) _showPinsBubble(false);
else _hidePinsBubble(true);
} catch {}
}
function onPinsBubbleDragMove(ev) {
try {
if (!pinsBubbleDrag.active) return;
if (pinsBubbleDrag.pointerId != null && ev.pointerId != null && ev.pointerId !== pinsBubbleDrag.pointerId) return;
const mapEl = _getMapElForPins();
if (!mapEl || !pinsBubbleEl || !document.contains(pinsBubbleEl)) return;
const mapRect = mapEl.getBoundingClientRect();
const dx = ev.clientX - pinsBubbleDrag.startX;
const dy = ev.clientY - pinsBubbleDrag.startY;
const left = Math.round(pinsBubbleDrag.baseLeft + dx);
const top = Math.round(pinsBubbleDrag.baseTop + dy);
const maxLeft = Math.max(0, Math.round(mapRect.width - 46));
const maxTop = Math.max(0, Math.round(mapRect.height - 46));
const cl = clamp(left, 0, maxLeft);
const ct = clamp(top, 0, maxTop);
pinsBubbleEl.style.left = cl + "px";
pinsBubbleEl.style.top = ct + "px";
if (!pinsBubbleDrag.moved) {
if (Math.abs(dx) > 3 || Math.abs(dy) > 3) {
pinsBubbleDrag.moved = true;
pinsBubbleDrag.movedAt = Date.now();
}
}
savePinsBubblePos({ left: cl, top: ct });
} catch {}
}
function onPinsBubbleDragUp(ev) {
try {
if (!pinsBubbleDrag.active) return;
if (pinsBubbleDrag.pointerId != null && ev.pointerId != null && ev.pointerId !== pinsBubbleDrag.pointerId) return;
pinsBubbleDrag.active = false;
const moved = !!pinsBubbleDrag.moved;
const movedAt = Number(pinsBubbleDrag.movedAt || 0);
try { document.removeEventListener("pointermove", onPinsBubbleDragMove, true); } catch {}
try { document.removeEventListener("pointerup", onPinsBubbleDragUp, true); } catch {}
try { document.removeEventListener("pointercancel", onPinsBubbleDragUp, true); } catch {}
try { if (pinsBubbleEl && ev.pointerId != null) pinsBubbleEl.releasePointerCapture(ev.pointerId); } catch {}
// If it was a tap (not a drag), restore the panel.
if (!moved) {
try { _restorePinsPanelFromBubble(); } catch {}
return;
}
// Ignore the synthetic click right after a drag.
pinsBubbleDrag.movedAt = movedAt || Date.now();
} catch {}
}
function _ensurePinsBubble(mapEl) {
try {
if (!mapEl) mapEl = _getMapElForPins();
if (!mapEl) return null;
if (pinsBubbleEl && document.contains(pinsBubbleEl)) return pinsBubbleEl;
const b = document.createElement("div");
b.className = "wmeRcPinsBubble";
b.title = "Map Pins";
b.innerHTML = `<span class="wmeRcI">${ICONS.mapPin}</span>`;
const pos = loadPinsBubblePos() || null;
if (pos) {
b.style.left = `${Math.max(0, Math.round(pos.left))}px`;
b.style.top = `${Math.max(0, Math.round(pos.top))}px`;
} else {
b.style.left = "12px";
b.style.top = "12px";
}
const stop = (ev) => { try { ev.stopPropagation(); } catch {} };
b.addEventListener("mousedown", stop, false);
b.addEventListener("click", stop, false);
b.addEventListener("dblclick", stop, false);
b.addEventListener("contextmenu", stop, false);
b.addEventListener("touchstart", stop, false);
b.addEventListener("pointerdown", (ev) => {
try {
if (ev.button != null && ev.button !== 0) return;
ev.preventDefault();
stop(ev);
const map = _getMapElForPins();
if (!map) return;
const mapRect = map.getBoundingClientRect();
const r = b.getBoundingClientRect();
pinsBubbleDrag.active = true;
pinsBubbleDrag.pointerId = ev.pointerId;
pinsBubbleDrag.startX = ev.clientX;
pinsBubbleDrag.startY = ev.clientY;
pinsBubbleDrag.baseLeft = r.left - mapRect.left;
pinsBubbleDrag.baseTop = r.top - mapRect.top;
pinsBubbleDrag.moved = false;
pinsBubbleDrag.movedAt = 0;
try { b.setPointerCapture(ev.pointerId); } catch {}
document.addEventListener("pointermove", onPinsBubbleDragMove, true);
document.addEventListener("pointerup", onPinsBubbleDragUp, true);
document.addEventListener("pointercancel", onPinsBubbleDragUp, true);
} catch {}
}, true);
mapEl.appendChild(b);
pinsBubbleEl = b;
_hidePinsBubble(true);
return b;
} catch { return null; }
}
function _showPinsBubble(withPulse) {
try {
const mapEl = _getMapElForPins();
const b = _ensurePinsBubble(mapEl);
// If we just restored from bubble, a short force-hide window may still be active.
// Clear it so the bubble cannot be hidden while we are minimizing.
try { _pinsBubbleForceHideUntil = 0; } catch {}
try { if (_pinsBubbleHideT) { clearTimeout(_pinsBubbleHideT); _pinsBubbleHideT = 0; } } catch {}
if (!b) return;
b.style.display = "flex";
try { b.classList.add("show"); } catch {}
try { b.classList.toggle("wmeRcPinsBubblePulse", !!withPulse); } catch {}
} catch {}
}
function _hidePinsBubble(instant) {
try {
if (!pinsBubbleEl || !document.contains(pinsBubbleEl)) return;
const b = pinsBubbleEl;
try { b.classList.remove("wmeRcPinsBubblePulse"); } catch {}
if (instant) {
try { b.classList.remove("show"); } catch {}
b.style.display = "none";
return;
}
try { b.classList.remove("show"); } catch {}
if (_pinsBubbleHideT) { try { clearTimeout(_pinsBubbleHideT); } catch {} }
_pinsBubbleHideT = setTimeout(() => {
_pinsBubbleHideT = 0;
try { if (b && document.contains(b)) b.style.display = "none"; } catch {}
}, 230);
} catch {}
}
function _setPinsPanelCollapsedFlag(isCollapsed) {
try {
if (!pinsPanelEl) return;
if (isCollapsed) {
pinsPanelEl.classList.add("collapsed");
pinsPanelEl.dataset.collapsed = "1";
try { pinsPanelEl.style.resize = "none"; } catch {}
savePinsPanelCollapsed(true);
} else {
pinsPanelEl.classList.remove("collapsed");
try { delete pinsPanelEl.dataset.collapsed; } catch {}
try { pinsPanelEl.style.resize = "vertical"; } catch {}
savePinsPanelCollapsed(false);
}
} catch {}
}
function _minimizePinsPanelInPlace(opts) {
try {
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return;
// Panel-mode collapse should never fade away: hard-reset any "bubble" animation artifacts.
try {
pinsPanelEl.classList.remove("animOut");
pinsPanelEl.classList.remove("animIn");
pinsPanelEl.style.display = "block";
pinsPanelEl.style.opacity = "";
pinsPanelEl.style.transform = "";
pinsPanelEl.style.pointerEvents = "";
pinsPanelEl.style.transition = "";
} catch {}
const instant = !!(opts && opts.instant);
// Remember current height so expanding can restore it.
try {
const h = Math.round(pinsPanelEl.getBoundingClientRect().height || 0);
if (h > 0) pinsPanelEl.dataset.prevExpandedHeight = String(h);
} catch {}
_setPinsPanelCollapsedFlag(true);
try { savePinsPanelMinimized(false); } catch {}
try { renderPinsPanel(); } catch {}
// Collapse down to header height (still a panel, not a bubble).
try {
pinsPanelEl.style.display = "block";
const hdr = pinsPanelEl.querySelector(".wmeRcPinsHdr");
const targetH = Math.max(44, Math.round((hdr ? hdr.getBoundingClientRect().height : 44) || 44));
if (instant) {
pinsPanelEl.style.height = targetH + "px";
} else {
const startH = Math.max(targetH, Math.round(pinsPanelEl.getBoundingClientRect().height || targetH));
pinsPanelEl.style.height = startH + "px";
pinsPanelEl.style.transition = "height .22s cubic-bezier(.2,.9,.2,1)";
requestAnimationFrame(() => {
try { pinsPanelEl.style.height = targetH + "px"; } catch {}
});
setTimeout(() => {
try { if (pinsPanelEl) pinsPanelEl.style.transition = ""; } catch {}
}, 240);
}
} catch {}
_hidePinsBubble(true);
try { _schedulePinsPanelAvoidDock(); } catch {}
} catch {}
}
function _restorePinsPanelFromPanelCollapse() {
try {
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return;
_setPinsPanelCollapsedFlag(false);
try { savePinsPanelMinimized(false); } catch {}
// Restore previous height (or last saved size) if we can.
try {
const sz = loadPinsPanelSize();
const prev = parseInt(String(pinsPanelEl.dataset.prevExpandedHeight || ""), 10);
const target = (sz && sz.h) ? Math.round(sz.h) : (Number.isFinite(prev) ? prev : 260);
const startH = Math.max(44, Math.round(pinsPanelEl.getBoundingClientRect().height || 44));
pinsPanelEl.style.height = startH + "px";
pinsPanelEl.style.transition = "height .24s cubic-bezier(.2,.9,.2,1)";
requestAnimationFrame(() => {
try { pinsPanelEl.style.height = target + "px"; } catch {}
});
setTimeout(() => {
try { if (pinsPanelEl) pinsPanelEl.style.transition = ""; } catch {}
}, 260);
} catch {
try { pinsPanelEl.style.height = ""; } catch {}
}
try { renderPinsPanel(); } catch {}
try { _schedulePinsPanelAvoidDock(); } catch {}
} catch {}
}
function _minimizePinsPanelToBubble(opts) {
try {
if (getPinsMinimizeMode() !== "bubble") { _minimizePinsPanelInPlace(opts); return; }
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return;
const mapEl = pinsPanelEl.parentElement || _getMapElForPins();
if (!mapEl) return;
_ensurePinsBubble(mapEl);
// Default bubble position: wherever the panel currently is.
try {
const mapRect = mapEl.getBoundingClientRect();
const r = pinsPanelEl.getBoundingClientRect();
const left = Math.round(r.left - mapRect.left);
const top = Math.round(r.top - mapRect.top);
const existing = loadPinsBubblePos();
if (!existing) savePinsBubblePos({ left, top });
if (pinsBubbleEl) {
const p = loadPinsBubblePos() || { left, top };
pinsBubbleEl.style.left = `${Math.max(0, Math.round(p.left))}px`;
pinsBubbleEl.style.top = `${Math.max(0, Math.round(p.top))}px`;
}
} catch {}
const instant = !!(opts && opts.instant);
_setPinsPanelCollapsedFlag(true);
try { savePinsPanelMinimized(true); } catch {}
try { renderPinsPanel(); } catch {}
// Animate panel out, then hide.
try {
if (!instant) {
pinsPanelEl.style.transition = "opacity .22s ease, transform .22s cubic-bezier(.2,.9,.2,1)";
pinsPanelEl.classList.add("animOut");
try { if (_pinsPanelMinimizeHideT) { clearTimeout(_pinsPanelMinimizeHideT); _pinsPanelMinimizeHideT = 0; } } catch {}
_pinsPanelMinimizeHideT = setTimeout(() => {
_pinsPanelMinimizeHideT = 0;
try { if (pinsPanelEl) { pinsPanelEl.style.display = "none"; pinsPanelEl.classList.remove("animOut"); pinsPanelEl.style.transition = ""; } } catch {}
}, 240);
} else {
pinsPanelEl.style.display = "none";
}
} catch {}
_showPinsBubble(true);
} catch {}
}
function _restorePinsPanelFromBubble() {
try {
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return;
// If a previous minimize-to-bubble animation is still pending, cancel its delayed hide.
try { if (_pinsPanelMinimizeHideT) { clearTimeout(_pinsPanelMinimizeHideT); _pinsPanelMinimizeHideT = 0; } } catch {}
// Force-hide the bubble during the opening sequence so it never overlaps the header.
try { _pinsBubbleForceHideUntil = _pinsNow() + 600; } catch { _pinsBubbleForceHideUntil = Date.now() + 600; }
// Restore the panel at its last saved panel position (not the bubble).
// Fallback: if no saved panel position exists yet, expand near the bubble.
try {
const mapEl = _getMapElForPins();
if (mapEl) {
const mapRect = mapEl.getBoundingClientRect();
const pad = 6;
const panelRect = pinsPanelEl.getBoundingClientRect();
const w = Math.round(panelRect.width || 260);
let target = null;
try {
const saved = JSON.parse(localStorage.getItem(PIN_POS_KEY) || "null");
if (saved && Number.isFinite(saved.left) && Number.isFinite(saved.top)) {
target = { left: saved.left, top: saved.top };
}
} catch {}
if (!target && pinsBubbleEl && document.contains(pinsBubbleEl)) {
const br = pinsBubbleEl.getBoundingClientRect();
target = { left: Math.round(br.left - mapRect.left), top: Math.round(br.top - mapRect.top) };
}
if (target) {
const maxLeft = Math.max(pad, Math.floor(mapRect.width - w - pad));
const maxTop = Math.max(pad, Math.floor(mapRect.height - 60));
const left = clamp(Math.round(target.left), pad, maxLeft);
const top = clamp(Math.round(target.top), pad, maxTop);
pinsPanelEl.style.left = `${left}px`;
pinsPanelEl.style.top = `${top}px`;
// Only persist if we had to clamp an existing saved position (viewport changed).
try {
if (target.left !== left || target.top !== top) {
localStorage.setItem(PIN_POS_KEY, JSON.stringify({ left, top }));
}
} catch {}
}
}
} catch {}
// Prevent the auto "avoid left dock" logic from overriding the bubble placement.
try { _pinsPanelSuppressDockUntil = _pinsNow() + 900; } catch {}
// Hide bubble immediately so it does not sit on top of the opening panel.
try { _hidePinsBubble(true); } catch {}
_setPinsPanelCollapsedFlag(false);
try { savePinsPanelMinimized(false); } catch {}
// Render expanded content first (so height/scroll are correct).
try { renderPinsPanel(); } catch {}
pinsPanelEl.style.display = "block";
try {
pinsPanelEl.style.transition = "opacity .24s ease, transform .24s cubic-bezier(.2,.9,.2,1)";
pinsPanelEl.classList.add("animOut");
requestAnimationFrame(() => {
try {
pinsPanelEl.classList.remove("animOut");
pinsPanelEl.classList.add("animIn");
setTimeout(() => {
try {
pinsPanelEl.classList.remove("animIn");
pinsPanelEl.style.transition = "";
} catch {}
}, 260);
} catch {}
});
} catch {}
try { _schedulePinsPanelAvoidDock(); } catch {}
} catch {}
}
function savePinsPanelCollapsed(v) {
try { localStorage.setItem(PIN_PANEL_COLLAPSED_KEY, v ? "1" : "0"); } catch {}
}
function getPinsPanelAlwaysVisibleEmpty() {
try { return localStorage.getItem(PIN_PANEL_ALWAYS_VISIBLE_EMPTY_KEY) === "1"; } catch { return false; }
}
function setPinsPanelAlwaysVisibleEmpty(v) {
try { localStorage.setItem(PIN_PANEL_ALWAYS_VISIBLE_EMPTY_KEY, v ? "1" : "0"); } catch {}
}
let _pinsPanelRO = null;
let _pinsPanelAutoSizing = false;
function _pinsPanelGetMapRect() {
try {
const mapEl = getMapContainerEl();
if (!mapEl) return null;
const mapRect = mapEl.getBoundingClientRect();
if (!mapRect || !Number.isFinite(mapRect.width) || !Number.isFinite(mapRect.height)) return null;
return { mapEl, mapRect };
} catch { return null; }
}
function _pinsPanelFitTopForHeight(desiredH) {
try {
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return;
const info = _pinsPanelGetMapRect();
if (!info) return;
const { mapRect } = info;
const pad = 6;
const topLimit = pad;
const bottomLimit = Math.max(pad, Math.floor(mapRect.height - pad));
let h = Number(desiredH);
if (!Number.isFinite(h) || h <= 0) {
h = Math.round(pinsPanelEl.getBoundingClientRect().height || 0);
}
const maxPossible = Math.max(160, bottomLimit - topLimit);
h = clamp(Math.round(h), 140, maxPossible);
const rect = pinsPanelEl.getBoundingClientRect();
let top = parseFloat(String(pinsPanelEl.style.top || ""));
if (!Number.isFinite(top)) top = rect.top - mapRect.top;
const needTop = bottomLimit - h;
if (top > needTop) top = needTop;
if (top < topLimit) top = topLimit;
pinsPanelEl.style.top = `${Math.round(top)}px`;
} catch {}
}
function applyPinsPanelHeightAuto() {
try {
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return;
if (pinsPanelEl.classList.contains("collapsed") || (pinsPanelEl.dataset && pinsPanelEl.dataset.collapsed === "1")) return;
updatePinsPanelHeightBounds();
const maxH = parseInt(String(pinsPanelEl.style.maxHeight || ""), 10);
const maxPx = Number.isFinite(maxH) ? maxH : (window.innerHeight - 24);
_pinsPanelLastAutoAt = _pinsNow();
_pinsPanelSuppressROUntil = _pinsPanelLastAutoAt + 650;
_pinsPanelAutoSizing = true;
pinsPanelEl.style.height = "auto";
const desired = clamp(Math.round(pinsPanelEl.scrollHeight || 0), 140, maxPx);
pinsPanelEl.style.height = `${desired}px`;
savePinsPanelSize({ h: desired, manual: false, u: 0 });
try { _pinsPanelFitTopForHeight(desired); } catch {}
} catch {}
finally { _pinsPanelAutoSizing = false; }
}
function updatePinsPanelHeightBounds() {
try {
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return;
const info = _pinsPanelGetMapRect();
if (!info) return;
const { mapRect } = info;
const pad = 6;
const topLimit = pad;
const bottomLimit = Math.max(pad, Math.floor(mapRect.height - pad));
const maxH = Math.max(160, bottomLimit - topLimit);
pinsPanelEl.style.maxHeight = `${maxH}px`;
try {
const manual = (pinsPanelEl.dataset && pinsPanelEl.dataset.manualHeight === "1");
const curH = Math.round(pinsPanelEl.getBoundingClientRect().height || 0);
if (curH > maxH) {
_pinsPanelLastAutoAt = _pinsNow();
_pinsPanelSuppressROUntil = _pinsPanelLastAutoAt + 650;
_pinsPanelAutoSizing = true;
pinsPanelEl.style.height = `${maxH}px`;
savePinsPanelSize({ h: maxH, manual: manual, u: manual ? 1 : 0 });
}
_pinsPanelFitTopForHeight(Math.min(curH || maxH, maxH));
} catch {} finally { _pinsPanelAutoSizing = false; }
} catch {}
}
let _pinsPanelResizeBound = false;
function attachMaxLen(inputEl, maxLen = 32) {
try {
if (!inputEl) return null;
const wrap = document.createElement("div");
wrap.className = "wmeRcLenHost";
try { inputEl.classList.add("wmeRcHasCount"); } catch {}
const count = document.createElement("div");
count.className = "wmeRcLenCountIn";
count.textContent = `0/${maxLen}`;
const msg = document.createElement("div");
msg.className = "wmeRcLimitMsg";
msg.textContent = `You can't enter more than ${maxLen} characters`;
msg.style.display = "none";
wrap.appendChild(inputEl);
wrap.appendChild(count);
let overAttempt = false;
const update = () => {
const v = String(inputEl.value || "");
const shownLen = Math.min(v.length, maxLen);
count.textContent = `${shownLen}/${maxLen}`;
if (v.length > maxLen) {
inputEl.value = v.slice(0, maxLen);
overAttempt = true;
}
if (overAttempt) {
try { inputEl.classList.add("bad"); } catch {}
msg.style.display = "block";
} else {
try { inputEl.classList.remove("bad"); } catch {}
msg.style.display = "none";
}
if (String(inputEl.value || "").length < maxLen) overAttempt = false;
};
inputEl.addEventListener("input", update);
inputEl.addEventListener("paste", () => setTimeout(update, 0));
update();
return { wrap, msg, update };
} catch {
return null;
}
}
function getMapContainerEl() {
return (
document.querySelector("#map") ||
document.querySelector("#WazeMap") ||
document.querySelector(".olMap") ||
document.querySelector(".wme-map") ||
null
);
}
function makePinSvgDataUri(hex, active) {
const c = normalizePinColor(hex);
const glow = active ? 0.55 : 0.28;
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="34" viewBox="0 0 34 34">
<defs>
<filter id="g" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2.6" result="b"/>
<feColorMatrix in="b" type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 ${glow} 0" result="c"/>
<feMerge>
<feMergeNode in="c"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<g filter="url(#g)">
<path d="M17 32s10-7.7 10-17a10 10 0 0 0-20 0c0 9.3 10 17 10 17z" fill="${c}"/>
<circle cx="17" cy="15" r="4.6" fill="rgba(255,255,255,.92)"/>
</g>
</svg>`;
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg.trim());
}
function makeClusterSvgDataUri(count, colors) {
const n = Math.max(2, Math.min(99, Number(count) || 2));
const txt = (n >= 99) ? "99+" : String(n);
const colsIn = Array.isArray(colors) ? colors.filter(Boolean).map(normalizePinColor) : [];
const uniq = [];
for (const c of colsIn) { if (!uniq.includes(c)) uniq.push(c); }
const maxStops = 8;
let sample = uniq.slice(0, maxStops);
if (uniq.length > maxStops) {
sample = [];
for (let i = 0; i < maxStops; i++) {
const idx = Math.round((i / (maxStops - 1)) * (uniq.length - 1));
sample.push(uniq[idx]);
}
}
if (sample.length < 2) sample = ["#2d9cff", "#7c5cff"];
let ar = 45, ag = 160, ab = 255;
try {
const src = colsIn.length ? colsIn : sample;
let r = 0, g = 0, b = 0;
for (const c of src) { const o = hexToRgb(c); r += o.r; g += o.g; b += o.b; }
ar = Math.round(r / src.length);
ag = Math.round(g / src.length);
ab = Math.round(b / src.length);
} catch {}
const stops = sample.map((c, i) => {
const off = (sample.length === 1) ? 0 : Math.round((i / (sample.length - 1)) * 100);
return `<stop offset="${off}%" stop-color="${c}"/>`;
}).join("");
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="38" height="38" viewBox="0 0 38 38">
<defs>
<filter id="s" x="-45%" y="-45%" width="190%" height="190%">
<feGaussianBlur stdDeviation="2.6" result="b"/>
<feColorMatrix in="b" type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 .55 0" result="c"/>
<feMerge><feMergeNode in="c"/><feMergeNode in="SourceGraphic"/></feMerge>
</filter>
<radialGradient id="g" cx="30%" cy="25%" r="85%">
${stops}
</radialGradient>
</defs>
<g filter="url(#s)">
<circle cx="19" cy="19" r="15.5" fill="url(#g)" stroke="rgba(255,255,255,.18)" stroke-width="1.2"/>
<circle cx="19" cy="19" r="11.2" fill="rgba(${ar},${ag},${ab},.10)"/>
<circle cx="19" cy="19" r="15.5" fill="rgba(0,0,0,.12)"/>
</g>
<text x="19" y="23" text-anchor="middle" font-family="system-ui,-apple-system,Segoe UI,Roboto,Arial" font-size="12" font-weight="900" fill="#fff">${txt}</text>
</svg>`;
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg.trim());
}
function clusterByPixel(items, distPx) {
const dist2 = (distPx || 36) * (distPx || 36);
const out = [];
const used = new Set();
for (let i = 0; i < items.length; i++) {
if (used.has(items[i].id)) continue;
const a = items[i];
const cluster = [a];
used.add(a.id);
for (let j = i + 1; j < items.length; j++) {
const b = items[j];
if (used.has(b.id)) continue;
const dx = (b.px.x - a.px.x);
const dy = (b.px.y - a.px.y);
if ((dx*dx + dy*dy) <= dist2) {
cluster.push(b);
used.add(b.id);
}
}
out.push(cluster);
}
return out;
}
function isRightClickEvt(evt) {
try {
const b = (typeof evt.button === "number") ? evt.button : null;
const w = (typeof evt.which === "number") ? evt.which : null;
return b === 2 || w === 3;
} catch {
return false;
}
}
function getClientXYFromEvt(evt, map) {
try {
const cx = Number(evt.clientX);
const cy = Number(evt.clientY);
if (Number.isFinite(cx) && Number.isFinite(cy)) return { x: cx, y: cy };
} catch {}
try {
const xy = evt && evt.xy;
if (xy && Number.isFinite(xy.x) && Number.isFinite(xy.y)) {
const div = map && (map.div || map.viewPortDiv || map.layerContainerDiv);
if (div && typeof div.getBoundingClientRect === "function") {
const r = div.getBoundingClientRect();
return { x: r.left + xy.x, y: r.top + xy.y };
}
}
} catch {}
try {
const px = Number(evt.pageX);
const py = Number(evt.pageY);
if (Number.isFinite(px) && Number.isFinite(py)) return { x: px, y: py };
} catch {}
return { x: 0, y: 0 };
}
function ensurePinsOlLayer() {
try {
const ol = UW?.OpenLayers;
const map = getOlMapBestEffort();
if (!ol || !map || typeof map.addLayer !== "function") return null;
if (!pinsOlLayer || !map.layers || !map.layers.includes(pinsOlLayer)) {
pinsOlLayer = new ol.Layer.Markers("Map Pins");
try { pinsOlLayer.displayInLayerSwitcher = false; } catch {}
try { pinsOlLayer.isBaseLayer = false; } catch {}
try { pinsOlLayer.uniqueName = `${SCRIPT_ID}:mapPins`; } catch {}
try { pinsOlLayer.setZIndex(9999); } catch {}
try {
const wm = UW?.W?.map;
if (wm && typeof wm.addLayer === "function") wm.addLayer(pinsOlLayer);
else map.addLayer(pinsOlLayer);
} catch { map.addLayer(pinsOlLayer); }
try { pinsOlLayer.setVisibility(getPinsLayerVisible()); } catch {}
try {
pinsOlLayer.events?.register?.("visibilitychanged", pinsOlLayer, () => {
try { setPinsLayerVisible(!!pinsOlLayer.getVisibility()); } catch {}
});
} catch {}
}
try {
const want = getPinsLayerVisible();
if (typeof pinsOlLayer.getVisibility === "function" && pinsOlLayer.getVisibility() !== want) {
pinsOlLayer.setVisibility(want);
}
} catch {}
return pinsOlLayer;
} catch {
return null;
}
}
let mapPinsLayersToggle = null;
let mapPinsLayersObs = null;
function syncMapPinsLayersToggle() {
try {
if (mapPinsLayersToggle) mapPinsLayersToggle.checked = !!getPinsLayerVisible();
} catch {}
}
function findDisplayCheckboxContainer() {
const wanted = [
/Satellite\s+imagery/i,
/Online\s+editors/i,
/GPS\s+points/i,
/Map\s+notes/i,
/Cities/i,
/Display/i,
];
try {
const sat = Array.from(document.querySelectorAll("label,span,div")).find((n) => {
try { return (n.textContent || "").trim() === "Satellite imagery"; } catch { return false; }
});
if (sat) {
let cur = sat;
for (let i = 0; i < 18 && cur; i++) {
const cbCount = cur.querySelectorAll?.('input[type="checkbox"]').length || 0;
if (cbCount >= 6) return cur;
cur = cur.parentElement;
}
}
} catch {}
const isVisible = (el) => {
try {
const r = el.getBoundingClientRect();
if (r.width < 80 || r.height < 80) return false;
const cs = getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden" || cs.opacity === "0") return false;
return true;
} catch { return false; }
};
const candidates = Array.from(document.querySelectorAll("div,section,ul")).filter(isVisible);
for (const el of candidates) {
const cb = el.querySelectorAll?.('input[type="checkbox"]').length || 0;
if (cb < 6) continue;
const t = (el.textContent || "");
let hits = 0;
for (const rx of wanted) { if (rx.test(t)) hits++; }
if (hits >= 2) return el;
}
const all = document.querySelectorAll("label,span,div");
let anchor = null;
for (const el of all) {
try {
const t = (el.textContent || "").trim();
if (!t) continue;
if (/Satellite\s+imagery/i.test(t) || /Online\s+editors/i.test(t) || t === "Display") { anchor = el; break; }
} catch {}
}
if (!anchor) return null;
let el = anchor;
for (let i = 0; i < 16 && el; i++) {
const cbCount = el.querySelectorAll?.('input[type="checkbox"]').length || 0;
if (cbCount >= 6) return el;
el = el.parentElement;
}
return null;
}
function ensureMapPinsToggleInLayers() {
try {
const container = findDisplayCheckboxContainer();
if (!container) return false;
const existing = container.querySelector('input[data-wme-rc-map-pins-toggle="1"]');
if (existing) {
mapPinsLayersToggle = existing;
syncMapPinsLayersToggle();
return true;
}
const row = document.createElement("div");
row.className = "wmeRcLayerRow";
const lab = document.createElement("label");
lab.className = "wmeRcLayerLabel";
const cb = document.createElement("input");
cb.type = "checkbox";
cb.checked = !!getPinsLayerVisible();
cb.setAttribute("data-wme-rc-map-pins-toggle", "1");
cb.addEventListener("change", () => {
setPinsLayerVisible(!!cb.checked);
try { renderPinsMarkers(); } catch {}
syncMapPinsLayersToggle();
});
const txt = document.createElement("span");
txt.textContent = "Map Pins";
lab.appendChild(cb);
lab.appendChild(txt);
row.appendChild(lab);
container.appendChild(row);
mapPinsLayersToggle = cb;
syncMapPinsLayersToggle();
return true;
} catch {
return false;
}
}
function startMapPinsLayersInjection() {
if (mapPinsLayersObs) return;
let tries = 0;
const iv = setInterval(() => {
tries++;
if (ensureMapPinsToggleInLayers() || tries > 20) {
try { clearInterval(iv); } catch {}
}
}, 600);
try {
mapPinsLayersObs = new MutationObserver(() => {
try { ensureMapPinsToggleInLayers(); } catch {}
});
mapPinsLayersObs.observe(document.body, { childList: true, subtree: true });
} catch {}
}
function ensurePinsMarkersLayer() {
const mapEl = getMapContainerEl();
if (!mapEl) return null;
try {
const cs = getComputedStyle(mapEl);
if (cs.position === "static") mapEl.style.position = "relative";
} catch {}
if (!pinsMarkersEl || !document.contains(pinsMarkersEl)) {
const el = document.createElement("div");
el.className = "wmeRcPinMarkers";
mapEl.appendChild(el);
pinsMarkersEl = el;
}
return pinsMarkersEl;
}
function getOlMapBestEffort() {
try {
const m = UW?.W?.map;
if (!m) return null;
if (typeof m.getOLMap === "function") return m.getOLMap();
if (m.olMap && typeof m.olMap.getViewPortPxFromLonLat === "function") return m.olMap;
if (m.map && typeof m.map.getViewPortPxFromLonLat === "function") return m.map;
if (typeof m.getViewPortPxFromLonLat === "function") return m;
} catch {}
return null;
}
function getMapPixelFromLonLat(lon, lat) {
try {
const ol = UW?.OpenLayers;
const map = getOlMapBestEffort();
if (ol && map && typeof map.getViewPortPxFromLonLat === "function") {
let ll = new ol.LonLat(lon, lat);
try {
const dst = map.getProjectionObject?.() || map.projection || null;
const dstCode = String(dst?.projCode || dst?.getCode?.() || dst || "");
const needsTransform = /900913|3857|102113|102100/i.test(dstCode) && !/4326/i.test(dstCode);
if (needsTransform && ll && typeof ll.transform === "function") {
const src = new ol.Projection("EPSG:4326");
if (dst) ll.transform(src, dst);
}
} catch {}
const px = map.getViewPortPxFromLonLat(ll);
if (px && isFinite(px.x) && isFinite(px.y)) return { x: px.x, y: px.y };
}
} catch {}
try {
const fn = sdk?.Map?.getPixelFromLonLat;
if (typeof fn === "function") {
const px = fn({ lon, lat });
if (px && isFinite(px.x) && isFinite(px.y)) return { x: px.x, y: px.y };
}
} catch {}
return null;
}
function updatePinsMarkersPositions() {
if (pinsOlLayer) return;
if (!pinsMarkersEl) return;
if (!getPinsLayerVisible()) return;
const pins = loadPins();
const _z = getCurrentZoomBestEffort();
const _minZ = Number(getPinsNamesMinZoom());
const showLabels = !!getPinsShowNamesOnMap() && (!Number.isFinite(_z) || _z >= _minZ);
for (const pin of pins) {
const el = pinMarkerEls.get(String(pin.id));
if (!el) continue;
if (pin.hideOnMap === true) { el.style.display = "none"; continue; }
el.style.display = "";
try { upsertMapPinLabel(el, pin, showLabels); } catch {}
const px = getMapPixelFromLonLat(pin.lon, pin.lat);
if (!px) continue;
const x = Math.round(px.x - 10);
const y = Math.round(px.y - 28);
el.style.transform = `translate3d(${x}px, ${y}px, 0)`;
el.classList.toggle("wmeRcPinMarkerActive", !!(pin.reminderAt && !pin.reminderDone));
}
}
function renderPinsMarkers() {
const pins = loadPins();
const olLayer = ensurePinsOlLayer();
const wantVisible = getPinsLayerVisible();
if (olLayer) {
try { cleanupDomPinsMarkers(); } catch {}
try { if (typeof olLayer.setVisibility === "function") olLayer.setVisibility(!!wantVisible); } catch {}
if (!wantVisible) {
try {
for (const mk of pinsOlMarkers.values()) { try { olLayer.removeMarker(mk); } catch {} }
pinsOlMarkers.clear();
for (const mk of pinsOlClusterMarkers.values()) { try { olLayer.removeMarker(mk); } catch {} }
pinsOlClusterMarkers.clear();
} catch {}
return;
}
if (!pins || pins.length === 0) {
try {
for (const mk of pinsOlMarkers.values()) { try { olLayer.removeMarker(mk); } catch {} }
pinsOlMarkers.clear();
for (const mk of pinsOlClusterMarkers.values()) { try { olLayer.removeMarker(mk); } catch {} }
pinsOlClusterMarkers.clear();
} catch {}
return;
}
try {
const ol = UW?.OpenLayers;
const map = getOlMapBestEffort();
if (!ol || !map) return;
const keep = new Set();
const keepClusters = new Set();
const clusteredIds = new Set();
const items = [];
let zoom = null;
try {
if (typeof map.getZoom === "function") zoom = Number(map.getZoom());
else if (typeof map.zoom === "number") zoom = Number(map.zoom);
} catch {}
const doCluster = (Date.now() > pinsNoClusterUntil) && (pins.length > 1) && (Number.isFinite(zoom) ? (zoom < 12) : false);
const _minZ = Number(getPinsNamesMinZoom());
const showLabels = !!getPinsShowNamesOnMap() && (!Number.isFinite(zoom) || zoom >= _minZ);
for (const pin of pins) {
if (pin.hideOnMap === true) continue;
const id = String(pin.id);
let ll = new ol.LonLat(pin.lon, pin.lat);
try {
const dst = map.getProjectionObject?.() || map.projection || null;
const dstCode = String(dst?.projCode || dst?.getCode?.() || dst || "");
const needsTransform = /900913|3857|102113|102100/i.test(dstCode) && !/4326/i.test(dstCode);
if (needsTransform && typeof ll.transform === "function") {
const src = new ol.Projection("EPSG:4326");
if (dst) ll.transform(src, dst);
}
} catch {}
const px = (typeof map.getLayerPxFromLonLat === "function")
? map.getLayerPxFromLonLat(ll)
: map.getViewPortPxFromLonLat(ll);
if (!px || !isFinite(px.x) || !isFinite(px.y)) continue;
items.push({ pin, id, ll, px });
}
if (doCluster && items.length > 1) {
const clusters = clusterByPixel(items, 40);
for (const cl of clusters) {
if (!cl || cl.length < 2) continue;
const ids = cl.map((x) => x.id).sort();
const cid = "c:" + ids.join("|");
keepClusters.add(cid);
ids.forEach((x) => clusteredIds.add(x));
let lonSum = 0, latSum = 0;
for (const it of cl) { lonSum += Number(it.ll.lon); latSum += Number(it.ll.lat); }
const llc = new ol.LonLat(lonSum / cl.length, latSum / cl.length);
const url = makeClusterSvgDataUri(cl.length, cl.map((x) => x?.pin?.color).filter(Boolean));
let mk = pinsOlClusterMarkers.get(cid);
const needsRecreate = !mk || !mk.icon || mk.icon.url !== url;
if (needsRecreate) {
try { if (mk) olLayer.removeMarker(mk); } catch {}
const size = new ol.Size(38, 38);
const offset = new ol.Pixel(-19, -19);
const icon = new ol.Icon(url, size, offset);
mk = new ol.Marker(llc, icon);
mk.clusterId = cid;
mk.clusterLat = (cl[0]?.pin?.lat);
mk.clusterLon = (cl[0]?.pin?.lon);
mk.events?.register?.("mousedown", mk, (evt) => {
try { ol.Event.stop(evt); } catch {}
try { pinsNoClusterUntil = Date.now() + 1200; } catch {}
try {
let minLon = Infinity, minLat = Infinity, maxLon = -Infinity, maxLat = -Infinity;
for (const it of cl) {
const llx = it?.ll;
if (!llx) continue;
minLon = Math.min(minLon, Number(llx.lon));
maxLon = Math.max(maxLon, Number(llx.lon));
minLat = Math.min(minLat, Number(llx.lat));
maxLat = Math.max(maxLat, Number(llx.lat));
}
if (isFinite(minLon) && isFinite(minLat) && isFinite(maxLon) && isFinite(maxLat) && typeof ol.Bounds === "function") {
const b = new ol.Bounds(minLon, minLat, maxLon, maxLat);
try { if (typeof b.scale === "function") b.scale(1.7); } catch {}
if (typeof map.zoomToExtent === "function") map.zoomToExtent(b, true);
setTimeout(() => {
try {
if (typeof map.setCenter === "function") map.setCenter(llc, 15);
} catch {}
}, 90);
} else {
const z = (typeof map.getZoom === "function") ? Number(map.getZoom()) : null;
const target = Math.max((Number.isFinite(z) ? z : 12) + 3, 15);
if (typeof map.setCenter === "function") map.setCenter(llc, target);
}
setTimeout(() => { try { renderPinsMarkers(); } catch {} }, 250);
} catch {}
});
try { mk.icon?.imageDiv?.classList?.add("wmeRcOlMarker"); } catch {}
try {
const div = mk && mk.icon && mk.icon.imageDiv;
if (div && !div.__wmeRcClusterCtx) {
div.__wmeRcClusterCtx = true;
div.style.cursor = "pointer";
div.addEventListener("contextmenu", (ev) => {
try { ev.preventDefault(); ev.stopPropagation(); if (ev.stopImmediatePropagation) ev.stopImmediatePropagation(); } catch {}
try { pinsNoClusterUntil = Date.now() + 1200; } catch {}
zoomToCluster(Number(mk.clusterLat), Number(mk.clusterLon));
}, true);
}
} catch {}
olLayer.addMarker(mk);
pinsOlClusterMarkers.set(cid, mk);
} else {
try { mk.lonlat = llc; } catch {}
try { mk.moveTo?.(map.getLayerPxFromLonLat(llc)); } catch {}
}
}
for (const [cid, mk] of Array.from(pinsOlClusterMarkers.entries())) {
if (keepClusters.has(cid)) continue;
try { olLayer.removeMarker(mk); } catch {}
pinsOlClusterMarkers.delete(cid);
}
} else {
for (const [cid, mk] of Array.from(pinsOlClusterMarkers.entries())) {
try { olLayer.removeMarker(mk); } catch {}
pinsOlClusterMarkers.delete(cid);
}
}
for (const it of items) {
const pin = it.pin;
const id = it.id;
if (clusteredIds.has(id)) {
try {
const mkOld = pinsOlMarkers.get(id);
if (mkOld) olLayer.removeMarker(mkOld);
} catch {}
pinsOlMarkers.delete(id);
continue;
}
keep.add(id);
const active = !!(pin.reminderAt && !pin.reminderDone);
const url = makePinSvgDataUri(pin.color, active);
let mk = pinsOlMarkers.get(id);
const needsRecreate = !mk || !mk.icon || mk.icon.url !== url;
if (needsRecreate) {
try { if (mk) olLayer.removeMarker(mk); } catch {}
const size = new ol.Size(34, 34);
const offset = new ol.Pixel(-17, -34);
const icon = new ol.Icon(url, size, offset);
mk = new ol.Marker(it.ll, icon);
mk.pinId = id;
mk.events?.register?.("mousedown", mk, (evt) => {
try { ol.Event.stop(evt); } catch {}
try {
const p = loadPins().find((x) => String(x.id) === id);
if (!p) return;
zoomToLonLatExact(p.lon, p.lat, getDefaultPinZoom());
try { showPinHitPill(p.name || "Pinned place"); } catch {}
try { if ((evt && (evt.button === 0 || evt.which === 1)) || !evt) showPinHitPill(p.name || "Pinned place"); } catch {}
} catch {}
});
try { mk.icon?.imageDiv?.classList?.add("wmeRcOlMarker"); } catch {}
try {
const div = mk && mk.icon && mk.icon.imageDiv;
if (div && !div.__wmeRcPinCtx) {
div.__wmeRcPinCtx = true;
div.style.cursor = "pointer";
}
} catch {}
try { upsertMapPinLabel(div, pin, showLabels); } catch {}
olLayer.addMarker(mk);
pinsOlMarkers.set(id, mk);
} else {
try { mk.moveTo?.(map.getLayerPxFromLonLat(it.ll)); } catch {}
try { mk.lonlat = it.ll; } catch {}
try { const div = mk && mk.icon && mk.icon.imageDiv; upsertMapPinLabel(div, pin, showLabels); } catch {}
}
}// Remove stale markers
for (const [id, mk] of Array.from(pinsOlMarkers.entries())) {
if (keep.has(id)) continue;
try { olLayer.removeMarker(mk); } catch {}
pinsOlMarkers.delete(id);
}
} catch {}
return;
}
if (!wantVisible) {
stopPinsMarkersLoop();
try { for (const el of pinMarkerEls.values()) el.remove(); pinMarkerEls.clear(); } catch {}
return;
}
if (!pins || pins.length === 0) {
stopPinsMarkersLoop();
try { for (const el of pinMarkerEls.values()) el.remove(); pinMarkerEls.clear(); } catch {}
return;
}
const layer = ensurePinsMarkersLayer();
if (!layer) return;
const _z = getCurrentZoomBestEffort();
const _minZ = Number(getPinsNamesMinZoom());
const showLabelsDom = !!getPinsShowNamesOnMap() && (!Number.isFinite(_z) || _z >= _minZ);
const keep = new Set();
for (const pin of pins) {
if (pin.hideOnMap === true) continue;
const id = String(pin.id);
keep.add(id);
let el = pinMarkerEls.get(id);
if (!el || !document.contains(el)) {
el = document.createElement("div");
el.className = "wmeRcPinMarker";
el.dataset.pinId = id;
el.title = pin.name || "Pinned place";
try { applyPinMarkerColors(el, pin.color); } catch {}
const inner = document.createElement("div");
inner.className = "wmeRcPinMarkerInner";
el.appendChild(inner);
try { upsertMapPinLabel(el, pin, showLabelsDom); } catch {}
el.addEventListener("click", (ev) => {
try { ev.stopPropagation(); ev.preventDefault(); } catch {}
try {
const p = loadPins().find((x) => String(x.id) === id);
if (!p) return;
zoomToLonLatExact(p.lon, p.lat, getDefaultPinZoom());
try { showPinHitPill(p.name || "Pinned place"); } catch {}
} catch {}
});
layer.appendChild(el);
pinMarkerEls.set(id, el);
} else {
el.title = pin.name || "Pinned place";
try { applyPinMarkerColors(el, pin.color); } catch {}
try { upsertMapPinLabel(el, pin, showLabelsDom); } catch {}
}
}
for (const [id, el] of Array.from(pinMarkerEls.entries())) {
if (keep.has(id)) continue;
try { el.remove(); } catch {}
pinMarkerEls.delete(id);
}
startPinsMarkersLoop();
}
function startPinsMarkersLoop() {
if (pinsMarkersIntervalId) return;
// Adaptive loop: slower when tab hidden + runs only when pins are actually visible.
pinsMarkersIntervalId = _createAdaptiveLoop(() => { try { updatePinsMarkersPositions(); } catch {} }, 400, {
active: () => {
try {
// Only run when we have visible markers layer and at least one marker.
if (!pinsMarkersEl || !document.contains(pinsMarkersEl)) return false;
if (!pinMarkerEls || pinMarkerEls.size === 0) return false;
// If user turned off pins on map, don't tick.
if (!getPinsOnMapEnabled()) return false;
return true;
} catch { return true; }
}
});
try { updatePinsMarkersPositions(); } catch {}
}
function stopPinsMarkersLoop() {
if (!pinsMarkersIntervalId) return;
try { pinsMarkersIntervalId(); } catch {}
pinsMarkersIntervalId = null;
}
function cleanupDomPinsMarkers() {
try { stopPinsMarkersLoop(); } catch {}
try { for (const el of pinMarkerEls.values()) { try { el.remove(); } catch {} } pinMarkerEls.clear(); } catch {}
try { if (pinsMarkersEl && pinsMarkersEl.parentNode) pinsMarkersEl.parentNode.removeChild(pinsMarkersEl); } catch {}
pinsMarkersEl = null;
}
function onPinsDragMove(ev) {
if (!pinsPanelEl || !pinsDrag.active) return;
if (pinsDrag.pointerId != null && ev.pointerId != null && ev.pointerId !== pinsDrag.pointerId) return;
try {
ev.preventDefault();
ev.stopPropagation();
ev.stopImmediatePropagation && ev.stopImmediatePropagation();
} catch {}
const mapEl =
document.querySelector("#map") ||
document.querySelector("#WazeMap") ||
document.querySelector(".olMap") ||
document.querySelector(".wme-map") ||
null;
if (!mapEl) return;
const mapRect = mapEl.getBoundingClientRect();
const r = pinsPanelEl.getBoundingClientRect();
const dx = ev.clientX - pinsDrag.startX;
const dy = ev.clientY - pinsDrag.startY;
let left = pinsDrag.baseLeft + dx;
let top = pinsDrag.baseTop + dy;
const pad = 6;
left = clamp(left, pad, Math.max(pad, mapRect.width - r.width - pad));
top = clamp(top, pad, Math.max(pad, mapRect.height - r.height - pad));
pinsPanelEl.style.left = `${left}px`;
pinsPanelEl.style.top = `${top}px`;
try { updatePinsPanelHeightBounds(); } catch {}
}
function onPinsDragUp(ev) {
if (!pinsPanelEl || !pinsDrag.active) return;
if (pinsDrag.pointerId != null && ev.pointerId != null && ev.pointerId !== pinsDrag.pointerId) return;
pinsDrag.active = false;
try {
document.removeEventListener("pointermove", onPinsDragMove, true);
document.removeEventListener("pointerup", onPinsDragUp, true);
document.removeEventListener("pointercancel", onPinsDragUp, true);
} catch {}
try {
const mapEl =
document.querySelector("#map") ||
document.querySelector("#WazeMap") ||
document.querySelector(".olMap") ||
document.querySelector(".wme-map") ||
null;
if (mapEl) {
const mapRect = mapEl.getBoundingClientRect();
const rect = pinsPanelEl.getBoundingClientRect();
const left = rect.left - mapRect.left;
const top = rect.top - mapRect.top;
localStorage.setItem(PIN_POS_KEY, JSON.stringify({ left: Math.round(left), top: Math.round(top) }));
}
} catch {}
try { pinsPanelEl.releasePointerCapture(pinsDrag.pointerId); } catch {}
pinsDrag.pointerId = null;
}
let _wmeRcTooltipEl = null;
let _wmeRcTooltipHideT = null;
function ensurePinNameTooltip() {
try {
if (_wmeRcTooltipEl && document.body.contains(_wmeRcTooltipEl)) return _wmeRcTooltipEl;
const el = document.createElement("div");
el.id = "wmeRcTooltip";
el.className = "wmeRcTooltip";
el.style.display = "none";
(document.body || document.documentElement).appendChild(el);
_wmeRcTooltipEl = el;
return el;
} catch {
return null;
}
}
function showPinNameTooltip(anchorEl, text) {
try {
const tip = ensurePinNameTooltip();
if (!tip || !anchorEl) return;
try { clearTimeout(_wmeRcTooltipHideT); } catch {}
tip.textContent = String(text || "");
tip.style.display = "block";
try { tip.classList.remove("show"); } catch {}
const place = () => {
const r = anchorEl.getBoundingClientRect();
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
const tr = tip.getBoundingClientRect();
const w = tr.width || Math.min(520, vw - 24);
const h = tr.height || 32;
let left = Math.max(8, Math.min(r.left || 0, vw - w - 8));
let top = (r.top || 0) - h - 10;
if (top < 8) top = Math.min(vh - h - 8, (r.bottom || 0) + 10);
tip.style.left = Math.round(left) + "px";
tip.style.top = Math.round(top) + "px";
};
requestAnimationFrame(() => {
try { place(); } catch {}
try { tip.classList.add("show"); } catch {}
});
} catch {}
}
let _wmeRcCountdownTipInt = 0;
let _wmeRcCountdownTipAnchor = null;
let _wmeRcCountdownTipAt = 0;
function getPinCountdownTooltipText(reminderAt, nowMs, mode){
try{
const at = Number(reminderAt);
const now = Number(nowMs);
if (!Number.isFinite(at) || !Number.isFinite(now)) return "";
const ms = at - now;
if (!Number.isFinite(ms) || ms <= 0) return "";
if (mode === 0) return getPinCountdownExact(at, now);
const totalSec = Math.max(0, Math.floor(ms / 1000));
const day = 86400;
const week = 7 * day;
const month = 30 * day;
const year = 365 * day;
let unit = "day";
let n = Math.floor(totalSec / day);
if (totalSec >= year){ unit = "year"; n = Math.floor(totalSec / year); }
else if (totalSec >= month){ unit = "month"; n = Math.floor(totalSec / month); }
else if (totalSec >= week){ unit = "week"; n = Math.floor(totalSec / week); }
else { unit = "day"; n = Math.floor(totalSec / day); }
const rem = totalSec % day;
const hh = String(Math.floor(rem / 3600)).padStart(2, "0");
const mi = String(Math.floor((rem % 3600) / 60)).padStart(2, "0");
const label = `${n} ${unit}${n === 1 ? "" : "s"}`;
return `${label} ${hh}:${mi}`;
}catch{ return ""; }
}
function showPinCountdownTooltip(anchorEl, reminderAtMs) {
try {
const at = Number(reminderAtMs);
if (!anchorEl || !Number.isFinite(at)) return;
_wmeRcCountdownTipAnchor = anchorEl;
_wmeRcCountdownTipAt = at;
const tick = () => {
try {
const now = Date.now();
const ms = at - now;
if (!Number.isFinite(ms) || ms <= 0) {
hidePinNameTooltip(0);
return;
}
const pinId = String((anchorEl && anchorEl.dataset && anchorEl.dataset.pinCountdown) || "");
const mode = (pinId && _pinCountdownTipModes && _pinCountdownTipModes.get(pinId)) || 0;
const t = getPinCountdownTooltipText(at, now, mode);
const tip = ensurePinNameTooltip();
if (tip && tip.style.display !== "none") {
tip.textContent = String(t || "");
} else {
showPinNameTooltip(anchorEl, t);
}
} catch {}
};
tick();
try { clearInterval(_wmeRcCountdownTipInt); } catch {}
_wmeRcCountdownTipInt = setInterval(() => {
try {
if (_wmeRcCountdownTipAnchor !== anchorEl) return;
tick();
} catch {}
}, 1000);
} catch {}
}
function hidePinNameTooltip(delay = 80) {
try {
try { clearInterval(_wmeRcCountdownTipInt); } catch {}
_wmeRcCountdownTipInt = 0;
_wmeRcCountdownTipAnchor = null;
_wmeRcCountdownTipAt = 0;
const tip = _wmeRcTooltipEl;
if (!tip) return;
try { clearTimeout(_wmeRcTooltipHideT); } catch {}
_wmeRcTooltipHideT = setTimeout(() => {
try { tip.classList.remove("show"); } catch {}
setTimeout(() => { try { tip.style.display = "none"; } catch {} }, 140);
}, Math.max(0, Number(delay) || 0));
} catch {}
let _pinsPanelDockObs = null;
let _pinsPanelAvoidDockTimer = 0;
let _pinsPanelAvoidDockRAF = 0;
function _findVisibleLeftDockRect() {
const candidates = [
"#sidepanel",
"#sidebar",
"#left-sidebar",
"#leftSidebar",
"aside",
"[class*='sidebar']",
"[class*='SidePanel']",
"[class*='leftPanel']",
"[data-testid*='side']",
];
const seen = new Set();
const els = [];
for (const sel of candidates) {
try {
document.querySelectorAll(sel).forEach(el => { if (el && !seen.has(el)) { seen.add(el); els.push(el); } });
} catch {}
}
let best = null;
for (const el of els) {
try {
const st = getComputedStyle(el);
if (st.display === "none" || st.visibility === "hidden" || Number(st.opacity) === 0) continue;
const r = el.getBoundingClientRect();
if (r.width < 220 || r.height < 200) continue;
if (r.right < 120) continue;
if (r.left > 40) continue; // must be docked left-ish
if (pinsPanelEl && (el === pinsPanelEl || el.contains(pinsPanelEl) || pinsPanelEl.contains(el))) continue;
const score = (Math.min(r.width, 520) * 2) + Math.min(r.height, 900);
if (!best || score > best.score) best = { el, r, score };
} catch {}
}
return best ? best.r : null;
}
function _rectsIntersect(a, b, pad) {
try {
const p = Number(pad) || 0;
return !(a.right <= b.left + p || a.left >= b.right - p || a.bottom <= b.top + p || a.top >= b.bottom - p);
} catch { return false; }
}
function _findVisibleBlockingPopupRects(mapRect) {
const sels = [
"[role='dialog']",
"[aria-modal='true']",
".issue-details, .issueDetails, .issue-details__panel, .issue-details__modal",
".wme__modal, .wme-modal, .wm-modal, .wm-dialog, .waze-modal",
".ReactModal__Content, .ReactModal__Overlay",
"[class*='modal']",
"[class*='Modal']",
"[class*='popup']",
"[class*='Popup']",
"[class*='dialog']",
"[class*='Dialog']",
"[data-testid*='modal']",
"[data-testid*='dialog']",
"[data-testid*='issue']",
];
const seen = new Set();
const els = [];
for (const sel of sels) {
try {
document.querySelectorAll(sel).forEach(el => {
if (!el || seen.has(el)) return;
seen.add(el);
els.push(el);
});
} catch {}
}
function _effectiveZIndex(el) {
try {
let cur = el;
for (let i = 0; i < 6 && cur; i++) {
const st = getComputedStyle(cur);
const zi = parseInt(st.zIndex, 10);
if (Number.isFinite(zi)) return zi;
cur = cur.parentElement;
}
} catch {}
return 0;
}
const out = [];
for (const el of els) {
try {
if (!el || !document.contains(el)) continue;
if (pinsPanelEl && (el === pinsPanelEl || el.contains(pinsPanelEl) || pinsPanelEl.contains(el))) continue;
if (String(el.className || "").includes("wmeRc")) continue;
const st = getComputedStyle(el);
if (st.display === "none" || st.visibility === "hidden" || Number(st.opacity) === 0) continue;
const r = el.getBoundingClientRect();
if (r.width < 260 || r.height < 140) continue;
if (mapRect) {
if (!_rectsIntersect(r, mapRect, 4)) continue;
}
if (mapRect && r.top > mapRect.bottom - 60) continue;
if (mapRect && r.width > mapRect.width * 0.98 && r.height > mapRect.height * 0.98) continue;
const z = _effectiveZIndex(el);
out.push({ r, z, area: r.width * r.height });
} catch {}
}
out.sort((a, b) => (b.z - a.z) || (b.area - a.area));
return out.map(x => x.r);
}
function _avoidPinsPanelLeftDockNow() {
try {
if (!pinsPanelEl || !document.contains(pinsPanelEl)) return;
if (pinsPanelEl.classList.contains("hidden")) return;
const mapEl = pinsPanelEl.parentElement;
if (!mapEl) return;
const mapRect = mapEl.getBoundingClientRect();
const margin = 12;
const panelRect = pinsPanelEl.getBoundingClientRect();
const panelW = panelRect.width || pinsPanelEl.offsetWidth || 0;
const panelH = panelRect.height || pinsPanelEl.offsetHeight || 0;
const mapW = mapRect.width || mapEl.clientWidth || 0;
const curLeft = panelRect.left - mapRect.left;
const curTop = panelRect.top - mapRect.top;
const maxLeft = Math.max(0, mapW - panelW - 8);
let targetLeft = curLeft;
const dockRect = _findVisibleLeftDockRect();
if (dockRect) {
const boundary = Math.max(0, dockRect.right - mapRect.left) + margin;
if (targetLeft + 6 < boundary) targetLeft = Math.min(maxLeft, Math.max(boundary, targetLeft));
}
const popRects = _findVisibleBlockingPopupRects(mapRect);
if (popRects && popRects.length) {
for (let iter = 0; iter < 2; iter++) {
const candRect = {
left: mapRect.left + targetLeft,
right: mapRect.left + targetLeft + panelW,
top: panelRect.top,
bottom: panelRect.top + panelH,
width: panelW,
height: panelH
};
let moved = false;
for (const pr of popRects) {
if (!_rectsIntersect(candRect, pr, 6)) continue;
const rightOf = (pr.right - mapRect.left) + margin;
if (rightOf <= maxLeft) {
if (targetLeft < rightOf) { targetLeft = rightOf; moved = true; }
continue;
}
const leftOf = (pr.left - mapRect.left) - panelW - margin;
if (leftOf >= 0) {
if (targetLeft > leftOf) { targetLeft = leftOf; moved = true; }
} else {
targetLeft = maxLeft;
moved = true;
}
}
if (!moved) break;
targetLeft = Math.max(0, Math.min(maxLeft, targetLeft));
}
}
targetLeft = Math.round(Math.max(0, Math.min(maxLeft, targetLeft)));
if (Math.abs(targetLeft - curLeft) > 1) {
_pinsPanelLastAutoAt = _pinsNow();
_pinsPanelSuppressROUntil = _pinsPanelLastAutoAt + 650;
pinsPanelEl.style.left = `${targetLeft}px`;
try { localStorage.setItem(PIN_POS_KEY, JSON.stringify({ left: targetLeft, top: Math.round(curTop) })); } catch {}
}
} catch {}
}
function _schedulePinsPanelAvoidDock() {
try {
if (_pinsPanelAvoidDockTimer) clearTimeout(_pinsPanelAvoidDockTimer);
_pinsPanelAvoidDockTimer = setTimeout(() => {
_pinsPanelAvoidDockTimer = 0;
if (_pinsPanelAvoidDockRAF) cancelAnimationFrame(_pinsPanelAvoidDockRAF);
_pinsPanelAvoidDockRAF = requestAnimationFrame(() => {
_pinsPanelAvoidDockRAF = 0;
_avoidPinsPanelLeftDockNow();
});
}, 80);
} catch {}
}
function _ensurePinsPanelDockObserver() {
try {
if (_pinsPanelDockObs || typeof MutationObserver === "undefined") return;
_pinsPanelDockObs = new MutationObserver(() => {
_schedulePinsPanelAvoidDock();
});
_pinsPanelDockObs.observe(document.body, { childList: true, subtree: true, attributes: true });
window.addEventListener("resize", () => { _schedulePinsPanelAvoidDock(); }, { passive: true });
} catch {}
}
}
function renderPinsPanel() {
if (!pinsPanelEl) return;
const allPins = loadPins();
pinsCache = allPins;
try { renderPinsMarkers(); } catch {}
const alwaysVisibleEmpty = getPinsPanelAlwaysVisibleEmpty();
pinsPanelEl.classList.toggle("hidden", allPins.length === 0 && !alwaysVisibleEmpty);
if (allPins.length === 0 && !alwaysVisibleEmpty) {
stopPinsCountdownLoop();
pinsPanelEl.innerHTML = "";
try { _pinCountdownEls.clear(); _pinRowEls.clear(); } catch {}
try { renderPinsMarkers(); } catch {}
return;
}
if (allPins.length === 0 && alwaysVisibleEmpty) {
stopPinsCountdownLoop();
try { _pinCountdownEls.clear(); _pinRowEls.clear(); } catch {}
}
pinsPanelEl.innerHTML = "";
const isCollapsedPinsPanel = !!(pinsPanelEl.classList.contains("collapsed") || (pinsPanelEl.dataset && pinsPanelEl.dataset.collapsed === "1"));
const hdr = document.createElement("div");
hdr.className = "wmeRcPinsHdr";
const titleWrap = document.createElement("div");
titleWrap.className = "wmeRcPinsHdrLeft";
const titleEl = document.createElement("div");
titleEl.className = "wmeRcPinsTitle";
titleEl.textContent = "Map Pins";
titleWrap.appendChild(titleEl);
// Clicking the title should behave exactly like the collapse/minimize control,
// but without triggering the header drag. Use pointer events (capture) to be robust.
try { titleWrap.style.cursor = "pointer"; } catch {}
const _pinsTitleTap = { down:false, x:0, y:0, t:0 };
const _togglePinsFromTitle = () => {
try {
if (!pinsPanelEl) return;
const isCol = !!(pinsPanelEl && (pinsPanelEl.classList.contains("collapsed") || (pinsPanelEl.dataset && pinsPanelEl.dataset.collapsed === "1") || pinsPanelEl.style.display === "none"));
if (isCol) {
const mode = getPinsMinimizeMode();
if (mode === "panel") _restorePinsPanelFromPanelCollapse();
else _restorePinsPanelFromBubble();
} else {
const mode = getPinsMinimizeMode();
if (mode === "panel") _minimizePinsPanelInPlace({ instant: false });
else _minimizePinsPanelToBubble({ instant: false });
}
try { _syncPinsBubbleVisibility(); } catch {}
// Ensure the collapse icon/label stays in sync.
try { _syncPinsPanelCollapseBtn && _syncPinsPanelCollapseBtn(); } catch {}
} catch {}
};
const _onPinsTitleDown = (ev) => {
try {
// Left-click or touch only.
if (ev && ev.button != null && ev.button !== 0) return;
_pinsTitleTap.down = true;
_pinsTitleTap.x = ev.clientX || 0;
_pinsTitleTap.y = ev.clientY || 0;
_pinsTitleTap.t = Date.now();
try { ev.preventDefault(); } catch {}
try { ev.stopPropagation(); } catch {}
} catch {}
};
const _onPinsTitleUp = (ev) => {
try {
if (!_pinsTitleTap.down) return;
_pinsTitleTap.down = false;
const dx = (ev.clientX || 0) - _pinsTitleTap.x;
const dy = (ev.clientY || 0) - _pinsTitleTap.y;
const dist = Math.sqrt(dx*dx + dy*dy);
const dt = Date.now() - (_pinsTitleTap.t || 0);
// Treat as a tap/click only if there was no drag.
if (dist <= 6 && dt <= 650) _togglePinsFromTitle();
try { ev.preventDefault(); } catch {}
try { ev.stopPropagation(); } catch {}
} catch {}
};
// Capture so a drag handler on the header can't swallow it.
titleWrap.addEventListener("pointerdown", _onPinsTitleDown, true);
titleWrap.addEventListener("pointerup", _onPinsTitleUp, true);
titleWrap.addEventListener("pointercancel", () => { _pinsTitleTap.down = false; }, true);
// Fallback for environments without Pointer Events.
titleWrap.addEventListener("mousedown", _onPinsTitleDown, true);
titleWrap.addEventListener("mouseup", _onPinsTitleUp, true);
const rightWrap = document.createElement("div");
rightWrap.className = "wmeRcPinsHdrRight";
const countEl = document.createElement("div");
countEl.className = "wmeRcPinsCount";
countEl.textContent = String(allPins.length);
const addGroupBtn = document.createElement("div");
addGroupBtn.className = "wmeRcPinsHdrBtn";
addGroupBtn.title = "Create folder";
addGroupBtn.innerHTML = `<span class="wmeRcI">${ICONS.folderPlus}</span>`;
addGroupBtn.addEventListener("click", (ev) => {
try { ev.preventDefault(); } catch {}
ev.stopPropagation();
try {
openGroupNameModal({
title: "New folder",
placeholder: "Name",
okText: "Create",
onSubmit: (name, emoji) => {
const groups = loadPinGroups();
const id = `g-${_uid()}`;
groups.push({ id, name, emoji: (typeof emoji === 'string') ? emoji : '' });
savePinGroups(groups);
renderPinsPanel();
try { _schedulePinsPanelAvoidDock(); } catch {}
}
});
} catch {}
});
const settingsBtn = document.createElement("div");
settingsBtn.className = "wmeRcPinsHdrBtn";
settingsBtn.title = "Pin settings";
settingsBtn.innerHTML = `<span class="wmeRcI">${ICONS.gear}</span>`;
settingsBtn.addEventListener("click", (ev) => {
try { ev.preventDefault(); } catch {}
try { ev.stopPropagation(); } catch {}
try { openPinsSettingsModal(); } catch {}
});
const collapseBtn = document.createElement("div");
collapseBtn.className = "wmeRcPinsHdrBtn wmeRcPinsCollapseBtn";
const _pinsMode = getPinsMinimizeMode();
const _pinsIsCollapsed = !!(pinsPanelEl && (pinsPanelEl.classList.contains("collapsed") || (pinsPanelEl.dataset && pinsPanelEl.dataset.collapsed === "1")));
if (_pinsMode === "bubble" && !_pinsIsCollapsed) {
collapseBtn.title = "Minimize";
collapseBtn.innerHTML = `<span class="wmeRcI" style="display:flex;">${ICONS.minus}</span>`;
} else {
collapseBtn.title = _pinsIsCollapsed ? "Expand panel" : "Collapse panel";
collapseBtn.innerHTML = `<span class="wmeRcI" style="display:flex;transition:transform .16s ease;transform:${_pinsIsCollapsed ? "rotate(180deg)" : "rotate(0deg)"};">${ICONS.chevDown}</span>`;
}
collapseBtn.addEventListener("click", (ev) => {
try { ev.preventDefault(); } catch {}
try { ev.stopPropagation(); } catch {}
try {
const isCol = !!(pinsPanelEl && (pinsPanelEl.classList.contains("collapsed") || (pinsPanelEl.dataset && pinsPanelEl.dataset.collapsed === "1") || pinsPanelEl.style.display === "none"));
if (!pinsPanelEl) return;
if (isCol) {
const mode = getPinsMinimizeMode();
if (mode === "panel") _restorePinsPanelFromPanelCollapse();
else _restorePinsPanelFromBubble();
} else {
const mode = getPinsMinimizeMode();
if (mode === "panel") _minimizePinsPanelInPlace({ instant: false });
else _minimizePinsPanelToBubble({ instant: false });
}
try { _syncPinsBubbleVisibility(); } catch {}
} catch {}
});
rightWrap.appendChild(countEl);
rightWrap.appendChild(addGroupBtn);
rightWrap.appendChild(settingsBtn);
rightWrap.appendChild(collapseBtn);
hdr.appendChild(titleWrap);
hdr.appendChild(rightWrap);
if (!isCollapsedPinsPanel) {
const list = document.createElement("div");
list.className = "wmeRcPinsList";
try { list.classList.remove("scroll"); } catch {}
try { pinsPanelEl.classList.remove("scrollMode"); } catch {}
try {
// Allow the list to scroll. We only stop propagation so the map below doesn't zoom/pan while
// you scroll inside the panel.
if (!list.dataset.scrollBound) {
list.dataset.scrollBound = "1";
const stop = (e) => { try { e.stopPropagation(); } catch {} };
list.addEventListener("wheel", stop, { passive: true });
list.addEventListener("touchmove", stop, { passive: true });
list.addEventListener("scroll", () => {
try {
const st = (list.scrollTop || 0);
const hdrs = Array.from(list.querySelectorAll(".wmeRcPinsGroupHdr"));
let active = null;
for (const h of hdrs) {
// offsetTop is relative to the scrolling container's content
if ((h.offsetTop || 0) <= st + 1) active = h;
}
for (const h of hdrs) h.classList.remove("wmeRcSticky");
if (active && st > (active.offsetTop || 0)) active.classList.add("wmeRcSticky");
} catch {}
}, { passive: true });
try {
const st = (list.scrollTop || 0);
const hdrs = Array.from(list.querySelectorAll(".wmeRcPinsGroupHdr"));
let active = null;
for (const h of hdrs) {
if ((h.offsetTop || 0) <= st + 1) active = h;
}
for (const h of hdrs) h.classList.remove("wmeRcSticky");
if (active && st > (active.offsetTop || 0)) active.classList.add("wmeRcSticky");
} catch {}
}
} catch {}const _pinReorder = { active: false, pid: null, row: null, parent: null, placeholder: null, pointerId: null,
startY: 0, dy: 0, baseTop: 0, baseLeft: 0, width: 0, height: 0, raf: 0 };
let _pinDragState = { id: null, overId: null, placeAfter: true, overGroup: null };
const _getPinDragLayer = () => {
try {
let dl = document.getElementById("wmeRcPinDragLayer");
if (dl) return dl;
dl = document.createElement("div");
dl.id = "wmeRcPinDragLayer";
dl.style.position = "fixed";
dl.style.left = "0";
dl.style.top = "0";
dl.style.right = "0";
dl.style.bottom = "0";
dl.style.zIndex = "2147483646";
dl.style.pointerEvents = "none";
dl.style.background = "transparent";
document.body.appendChild(dl);
return dl;
} catch {
return document.body;
}
};
const _clearPinDropMarks = () => {
try {
list.querySelectorAll('.wmeRcPinRow.dropBefore,.wmeRcPinRow.dropAfter,.wmeRcPinsGroupHdr.dropTarget')
.forEach((el) => {
el.classList.remove('dropBefore');
el.classList.remove('dropAfter');
el.classList.remove('dropTarget');
});
} catch {}
};
const _movePinInArray = (arr, fromId, toId, after) => {
const fromIdx = arr.findIndex((p) => p.id === String(fromId));
const toIdxRaw = arr.findIndex((p) => p.id === String(toId));
if (fromIdx < 0 || toIdxRaw < 0 || fromId === toId) return arr;
const item = arr[fromIdx];
const next = arr.slice();
next.splice(fromIdx, 1);
const toIdx = next.findIndex((p) => p.id === String(toId));
const ins = after ? (toIdx + 1) : toIdx;
next.splice(Math.max(0, ins), 0, item);
return next;
};
const _movePinToGroupEnd = (arr, pinId, groupId) => {
const pid = String(pinId);
const gid = normalizeGroupId(groupId);
const idx = arr.findIndex(p => p && p.id === pid);
if (idx < 0) return arr;
const item = { ...arr[idx], groupId: gid };
const next = arr.slice();
next.splice(idx, 1);
let lastIdx = -1;
for (let i = 0; i < next.length; i++) {
if (normalizeGroupId(next[i].groupId) === gid) lastIdx = i;
}
next.splice(lastIdx + 1, 0, item);
return next;
};
const groups = loadPinGroups();
const seen = new Set(groups.map(g => g.id));
for (const p of allPins) {
const gid = normalizeGroupId(p.groupId);
if (!seen.has(gid)) {
groups.push({ id: gid, name: getGroupName(gid) });
seen.add(gid);
}
}
const buildPinRow = (pin, groupId) => {
const row = document.createElement("div");
row.className = "wmeRcPinRow";
row.dataset.pinId = String(pin.id);
row.dataset.groupId = normalizeGroupId(groupId);
try { _pinRowEls.set(String(pin.id), row); } catch {}
try {
const base = hexToRgb(pin.color);
row.style.setProperty("--pinRGB", `${base.r},${base.g},${base.b}`);
} catch {}
if (pin.reminderAt && !pin.reminderDone) row.classList.add("wmeRcPinRowActive");
const left = document.createElement("div");
left.className = "wmeRcPinLeft";
const name = document.createElement("div");
name.className = "wmeRcPinName";
name.textContent = pin.name || "Pinned place";
const _pn = String(pin && pin.name ? pin.name : "");
const _needsRename = (_pn.trim().length > PIN_NAME_MAX);
if (_needsRename) {
try { row.classList.add("wmeRcPinNeedsRename"); } catch {}
}
name.addEventListener("mouseenter", () => {
try {
if ((name.scrollWidth || 0) > (name.clientWidth || 0) + 2) {
showPinNameTooltip(name, name.textContent || "");
}
} catch {}
});
name.addEventListener("mouseleave", () => { try { hidePinNameTooltip(0); } catch {} });
const sub = document.createElement("div");
sub.className = "wmeRcPinSub";
const cdLine = document.createElement("div");
cdLine.className = "wmeRcPinCountdownLine";
const bad = document.createElement("span");
bad.className = "wmeRcPinBadName";
bad.textContent = "Rename";
if (!_needsRename) bad.style.display = "none";
const cdSpan = document.createElement("span");
cdSpan.className = "wmeRcPinCountdownBadge";
cdSpan.dataset.pinCountdown = pin.id;
try { _pinCountdownEls.set(String(pin.id), cdSpan); } catch {}
cdSpan.textContent = getPinCountdownTextMode(pin.reminderAt, Date.now(), _pinCountdownModes.get(pin.id) || 0);
cdSpan.addEventListener("mouseenter", () => {
try {
if (!pin || !pin.reminderAt || pin.reminderDone) return;
const now = Date.now();
const ms = Number(pin.reminderAt) - now;
if (!Number.isFinite(ms) || ms <= 0) return;
if (ms >= 86400000) {
showPinCountdownTooltip(cdSpan, pin.reminderAt);
}
} catch {}
});
cdSpan.addEventListener("mouseleave", () => { try { hidePinNameTooltip(0); } catch {} });
cdSpan.addEventListener("click", (e) => {
try { e.preventDefault(); } catch {}
try { e.stopPropagation(); } catch {}
try { e.stopImmediatePropagation(); } catch {}
const pid = String(cdSpan.dataset.pinCountdown || "");
if (!pid) return false;
const curTip = (_pinCountdownTipModes.get(pid) || 0);
const nextTip = (curTip + 1) % 2;
_pinCountdownTipModes.set(pid, nextTip);
try { if (pin && pin.reminderAt && !pin.reminderDone) showPinCountdownTooltip(cdSpan, pin.reminderAt); } catch {}
return false;
});
cdLine.appendChild(cdSpan);
try { sub.insertBefore(bad, cdLine); } catch { try { sub.appendChild(bad); } catch {} }
sub.appendChild(cdLine);
if (!pin.reminderAt || pin.reminderDone) cdLine.style.display = "none";
left.appendChild(name);
left.appendChild(sub);
const btns = document.createElement("div");
btns.className = "wmeRcPinBtns";
const mkBtn = (icon, title, onClick) => {
const b = document.createElement("div");
b.className = "wmeRcPinBtn";
b.title = title;
b.innerHTML = `<span class="wmeRcI">${icon}</span>`;
b.addEventListener("click", (ev) => { ev.stopPropagation(); onClick(); });
return b;
};
const eyeBtn = mkBtn(ICONS.eye, "Hide on map", () => {
try {
const newHide = !(pin.hideOnMap === true);
updatePin(pin.id, { hideOnMap: newHide });
} catch {}
});
eyeBtn.classList.add("wmeRcPinBtnHoverOnly","wmeRcPinBtnEye");
if (pin.hideOnMap === true) eyeBtn.classList.add("isHidden");
btns.appendChild(eyeBtn);
const editBtn = mkBtn(ICONS.edit, "Edit", () => openRenamePinModal(pin.id));
editBtn.classList.add("wmeRcPinBtnHoverOnly","wmeRcPinBtnEdit");
btns.appendChild(editBtn);
const trashBtn = mkBtn(ICONS.trash, "Remove", () => confirmRemovePin(pin.id));
trashBtn.classList.add("wmeRcPinBtnHoverOnly","wmeRcPinBtnTrash");
btns.appendChild(trashBtn);
const bellBtn = mkBtn(ICONS.bell, "Set reminder", () => openReminderModal(pin.id));
bellBtn.classList.add("wmeRcPinBtnBell");
btns.appendChild(bellBtn);
const beginReorder = (ev) => {
try {
if (!ev || (ev.button != null && ev.button !== 0)) return;
ev.preventDefault(); ev.stopPropagation();
} catch {}
try {
if (!_pinReorder || _pinReorder.active) return;
const sourceBody = row.parentElement;
if (!sourceBody || !sourceBody.classList.contains("wmeRcPinsGroupBody")) return;
_pinReorder.active = true;
_pinReorder.pid = String(pin.id);
_pinReorder.row = row;
_pinReorder.sourceBody = sourceBody;
_pinReorder.currentBody = sourceBody;
_pinReorder.pointerId = ev.pointerId;
_pinReorder.sourceGroupId = normalizeGroupId(row.dataset.groupId || sourceBody.dataset.groupId || "");
_pinReorder.currentGroupId = _pinReorder.sourceGroupId;
_pinReorder.expandTimer = 0;
_pinReorder.expandGid = null;
_pinReorder.dropHdr = null;
const r = row.getBoundingClientRect();
_pinReorder.width = r.width;
_pinReorder.height = r.height;
_pinReorder.grabX = ev.clientX - r.left;
_pinReorder.grabY = ev.clientY - r.top;
_pinReorder.lastX = ev.clientX;
_pinReorder.lastY = ev.clientY;
const ph = document.createElement("div");
ph.className = "wmeRcPinPlaceholder";
ph.style.height = Math.max(34, Math.round(r.height)) + "px";
_pinReorder.placeholder = ph;
try { sourceBody.insertBefore(ph, row); } catch {}
try { row.remove(); } catch {}
const ghost = row.cloneNode(true);
ghost.classList.add("wmeRcPinGhost");
ghost.classList.remove("reorderFloat");
ghost.style.position = "fixed";
ghost.style.left = "0";
ghost.style.top = "0";
ghost.style.margin = "0";
ghost.style.width = Math.round(r.width) + "px";
ghost.style.height = Math.round(r.height) + "px";
ghost.style.pointerEvents = "none";
ghost.style.opacity = "0.95";
ghost.style.transform = `translate3d(${Math.round(r.left)}px, ${Math.round(r.top)}px, 0)`;
ghost.style.willChange = "transform";
_pinReorder.ghost = ghost;
const dragLayer = _getPinDragLayer();
try { dragLayer.appendChild(ghost); } catch { try { document.body.appendChild(ghost); } catch {} }
try { document.documentElement.classList.add("wmeRcNoSelect"); } catch {}
try { window.addEventListener("pointermove", onReorderMove, true); } catch {}
try { window.addEventListener("pointerup", endReorder, true); } catch {}
try { window.addEventListener("pointercancel", endReorder, true); } catch {}
try { row.setPointerCapture(ev.pointerId); } catch {}
try {
const empty = sourceBody.querySelector(".wmeRcPinsGroupEmpty");
if (empty) empty.style.display = "none";
} catch {}
} catch (e) {
try { console.warn("[Pins] reorder begin failed", e); } catch {}
try { endReorder(); } catch {}
}
};
const _setDropHdr = (gid) => {
gid = normalizeGroupId(gid);
try {
if (_pinReorder.dropHdr && _pinReorder.dropHdr.dataset && normalizeGroupId(_pinReorder.dropHdr.dataset.groupId) === gid) return;
} catch {}
try { if (_pinReorder.dropHdr) _pinReorder.dropHdr.classList.remove("dropTarget"); } catch {}
_pinReorder.dropHdr = null;
if (!gid) return;
let hdr = null;
try { hdr = list.querySelector(`.wmeRcPinsGroupHdr[data-group-id="${CSS.escape(gid)}"]`); } catch {}
if (!hdr) { try { hdr = list.querySelector(`.wmeRcPinsGroupHdr[data-group-id="${gid}"]`); } catch {} }
if (hdr) {
_pinReorder.dropHdr = hdr;
try { hdr.classList.add("dropTarget"); } catch {}
}
};
const _findGroupUnderPointer = (x, y) => {
let el = null;
try { el = document.elementFromPoint(x, y); } catch {}
if (!el) return { body: null, gid: null, hdr: null };
const overRow = el.closest ? el.closest(".wmeRcPinRow") : null;
if (overRow) {
const body = overRow.closest ? overRow.closest(".wmeRcPinsGroupBody") : null;
const gid = normalizeGroupId(overRow.dataset.groupId || (body && body.dataset.groupId) || "");
const hdr = body && body.previousElementSibling && body.previousElementSibling.classList && body.previousElementSibling.classList.contains("wmeRcPinsGroupHdr")
? body.previousElementSibling : null;
return { body, gid, hdr, row: overRow };
}
const overBody = el.closest ? el.closest(".wmeRcPinsGroupBody") : null;
if (overBody) {
const gid = normalizeGroupId(overBody.dataset.groupId || "");
const hdr = overBody.previousElementSibling && overBody.previousElementSibling.classList && overBody.previousElementSibling.classList.contains("wmeRcPinsGroupHdr")
? overBody.previousElementSibling : null;
return { body: overBody, gid, hdr, row: null };
}
const overHdr = el.closest ? el.closest(".wmeRcPinsGroupHdr") : null;
if (overHdr) {
const gid = normalizeGroupId(overHdr.dataset.groupId || "");
let body = null;
try {
const wrap = overHdr.closest && overHdr.closest(".wmeRcPinsGroup");
body = wrap ? wrap.querySelector(".wmeRcPinsGroupBody") : null;
} catch {}
return { body, gid, hdr: overHdr, row: null };
}
return { body: null, gid: null, hdr: null, row: null };
};
const _ensureExpandedDuringHover = (gid) => {
gid = normalizeGroupId(gid);
if (!gid) return;
if (_pinReorder.expandGid === gid) return;
_pinReorder.expandGid = gid;
try { if (_pinReorder.expandTimer) clearTimeout(_pinReorder.expandTimer); } catch {}
_pinReorder.expandTimer = setTimeout(() => {
try {
const wrap = list.querySelector(`.wmeRcPinsGroup[data-group-id="${CSS.escape(gid)}"]`);
if (wrap && wrap.classList.contains("collapsed")) {
wrap.classList.remove("collapsed");
setGroupCollapsed(gid, false);
}
} catch {}
}, 180);
};
const _placePlaceholderInBody = (body, pointerY) => {
const ph = _pinReorder.placeholder;
if (!ph || !body) return;
try {
const empty = body.querySelector(".wmeRcPinsGroupEmpty");
if (empty) empty.style.display = "none";
} catch {}
const rows = Array.from(body.querySelectorAll(":scope > .wmeRcPinRow"));
let beforeEl = null;
for (const r of rows) {
try {
const rr = r.getBoundingClientRect();
const mid = rr.top + rr.height / 2;
if (pointerY < mid) { beforeEl = r; break; }
} catch {}
}
const key = (body.dataset && body.dataset.groupId ? body.dataset.groupId : "") + "|" + (beforeEl ? beforeEl.dataset.pinId : "END");
if (_pinReorder.lastPlaceKey === key) return;
_pinReorder.lastPlaceKey = key;
try {
if (beforeEl) body.insertBefore(ph, beforeEl);
else body.appendChild(ph);
} catch {}
};
const onReorderMove = (ev) => {
try {
if (!_pinReorder.active) return;
if (_pinReorder.pointerId != null && ev.pointerId != null && ev.pointerId !== _pinReorder.pointerId) return;
try { ev.preventDefault(); } catch {}
_pinReorder.lastX = ev.clientX;
_pinReorder.lastY = ev.clientY;
if (_pinReorder.raf) return;
_pinReorder.raf = requestAnimationFrame(() => {
_pinReorder.raf = 0;
const x = (_pinReorder.lastX ?? 0) - (_pinReorder.grabX ?? 0);
const y = (_pinReorder.lastY ?? 0) - (_pinReorder.grabY ?? 0);
try { if (_pinReorder.ghost) _pinReorder.ghost.style.transform = `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)`; } catch {}
const hit = _findGroupUnderPointer(_pinReorder.lastX, _pinReorder.lastY);
const body = hit.body || _pinReorder.currentBody;
const gid = normalizeGroupId(hit.gid || (_pinReorder.currentBody && _pinReorder.currentBody.dataset.groupId) || _pinReorder.currentGroupId || "");
if (gid) {
_setDropHdr(gid);
_ensureExpandedDuringHover(gid);
}
if (body && body !== _pinReorder.currentBody) {
_pinReorder.currentBody = body;
_pinReorder.currentGroupId = gid;
}
if (body) {
_placePlaceholderInBody(body, _pinReorder.lastY);
}
});
} catch {}
};
const _syncGroupCount = (gid) => {
gid = normalizeGroupId(gid);
if (!gid) return;
try {
const body = list.querySelector(`.wmeRcPinsGroupBody[data-group-id="${CSS.escape(gid)}"]`);
const hdr = list.querySelector(`.wmeRcPinsGroupHdr[data-group-id="${CSS.escape(gid)}"]`);
if (!body || !hdr) return;
const c = hdr.querySelector(".wmeRcPinsGroupCount");
if (c) c.textContent = String(body.querySelectorAll(":scope > .wmeRcPinRow").length);
} catch {}
};
const _syncEmptyHint = (gid) => {
gid = normalizeGroupId(gid);
if (!gid) return;
let body = null;
try { body = list.querySelector(`.wmeRcPinsGroupBody[data-group-id="${CSS.escape(gid)}"]`); } catch {}
if (!body) return;
const hasRows = body.querySelectorAll(":scope > .wmeRcPinRow").length > 0;
let empty = body.querySelector(".wmeRcPinsGroupEmpty");
if (hasRows) {
if (empty) { try { empty.remove(); } catch {} }
} else {
if (!empty) {
empty = document.createElement("div");
empty.className = "wmeRcPinsGroupEmpty";
empty.textContent = "Drop pins here";
body.appendChild(empty);
}
try { empty.style.display = ""; } catch {}
}
};
const _persistPinsFromDOM = () => {
const pins = loadPins();
const byId = new Map(pins.map(p => [String(p.id), p]));
const next = [];
const bodies = Array.from(list.querySelectorAll(".wmeRcPinsGroupBody"));
for (const body of bodies) {
const gid = normalizeGroupId(body.dataset.groupId || "");
const rows = Array.from(body.querySelectorAll(":scope > .wmeRcPinRow"));
for (const r of rows) {
const id = String(r.dataset.pinId || "");
const p = byId.get(id);
if (!p) continue;
next.push({ ...p, groupId: gid });
byId.delete(id);
}
}
for (const p of byId.values()) next.push(p);
savePins(next);
};
const endReorder = (ev) => {
try {
if (!_pinReorder.active) return;
if (_pinReorder.pointerId != null && ev && ev.pointerId != null && ev.pointerId !== _pinReorder.pointerId) return;
} catch {}
try {
window.removeEventListener("pointermove", onReorderMove, true);
window.removeEventListener("pointerup", endReorder, true);
window.removeEventListener("pointercancel", endReorder, true);
} catch {}
try { if (_pinReorder.raf) cancelAnimationFrame(_pinReorder.raf); } catch {}
_pinReorder.raf = 0;
const ph = _pinReorder.placeholder;
const ghost = _pinReorder.ghost;
let destBody = _pinReorder.currentBody || _pinReorder.sourceBody;
if (!destBody || !destBody.classList || !destBody.classList.contains("wmeRcPinsGroupBody")) destBody = _pinReorder.sourceBody;
const destGid = normalizeGroupId((destBody && destBody.dataset && destBody.dataset.groupId) || _pinReorder.currentGroupId || _pinReorder.sourceGroupId || "");
const srcGid = normalizeGroupId(_pinReorder.sourceGroupId || "");
try {
if (ph && destBody) destBody.insertBefore(row, ph);
else if (destBody) destBody.appendChild(row);
} catch {}
try { if (ph) ph.remove(); } catch {}
try { if (ghost) ghost.remove(); } catch {}
try { document.documentElement.classList.remove("wmeRcNoSelect"); } catch {}
try { if (_pinReorder.dropHdr) _pinReorder.dropHdr.classList.remove("dropTarget"); } catch {}
try { row.dataset.groupId = destGid; } catch {}
try { _persistPinsFromDOM(); } catch {}
try { _syncEmptyHint(srcGid); } catch {}
try { _syncEmptyHint(destGid); } catch {}
try { _syncGroupCount(srcGid); } catch {}
try { _syncGroupCount(destGid); } catch {}
try {
_pinReorder.active = false;
_pinReorder.pid = null;
_pinReorder.row = null;
_pinReorder.sourceBody = null;
_pinReorder.currentBody = null;
_pinReorder.placeholder = null;
_pinReorder.ghost = null;
_pinReorder.pointerId = null;
_pinReorder.lastPlaceKey = null;
_pinReorder.sourceGroupId = null;
_pinReorder.currentGroupId = null;
try { if (_pinReorder.expandTimer) clearTimeout(_pinReorder.expandTimer); } catch {}
_pinReorder.expandTimer = 0;
_pinReorder.expandGid = null;
_pinReorder.dropHdr = null;
} catch {}
};
row.addEventListener("pointerdown", (ev) => {
try {
const t = ev && ev.target;
if (!t) return;
if (t.closest && t.closest(".wmeRcPinBtns")) return;
if (t.closest && t.closest("button, a, input, textarea, select, option, label")) return;
} catch {}
try {
if (!ev || (ev.button != null && ev.button !== 0)) return;
if (_pinReorder && _pinReorder.active) return;
const pid = ev.pointerId;
const sx = ev.clientX, sy = ev.clientY;
const downEv = ev;
let started = false;
const cleanup = () => {
try { window.removeEventListener("pointermove", onArmMove, true); } catch {}
try { window.removeEventListener("pointerup", onArmEnd, true); } catch {}
try { window.removeEventListener("pointercancel", onArmEnd, true); } catch {}
};
const onArmMove = (mv) => {
try {
if (pid != null && mv.pointerId != null && mv.pointerId !== pid) return;
const dx = mv.clientX - sx;
const dy = mv.clientY - sy;
if (!started && (Math.abs(dx) > 6 || Math.abs(dy) > 6)) {
started = true;
cleanup();
beginReorder(downEv);
try { onReorderMove(mv); } catch {}
}
} catch {}
};
const onArmEnd = (up) => {
try {
if (pid != null && up && up.pointerId != null && up.pointerId !== pid) return;
} catch {}
const didStart = started;
cleanup();
try {
if (didStart) return;
if (_pinReorder && _pinReorder.active) return;
const t = up && up.target;
if (!t) return;
if (t.closest && t.closest(".wmeRcPinBtns")) return;
if (t.closest && t.closest("button, a, input, textarea, select, option, label")) return;
const dx = (up.clientX - sx);
const dy = (up.clientY - sy);
if (Math.abs(dx) <= 6 && Math.abs(dy) <= 6) {
try { up.preventDefault(); up.stopPropagation(); } catch {}
try { if (row.classList.contains("dragging")) return; } catch {}
if (_needsRename) { toast(`Rename this pin (max ${PIN_NAME_MAX} chars)`); try { openRenamePinModal(pin.id); } catch {} return; }
jumpToPin(pin);
}
} catch {}
};
window.addEventListener("pointermove", onArmMove, true);
window.addEventListener("pointerup", onArmEnd, true);
window.addEventListener("pointercancel", onArmEnd, true);
} catch {}
}, true);
row.appendChild(left);
row.appendChild(btns);
return row;
};
for (const g of groups) {
const gid = normalizeGroupId(g.id);
const groupWrap = document.createElement("div");
groupWrap.className = "wmeRcPinsGroup";
groupWrap.dataset.groupId = gid;
if (isGroupCollapsed(gid)) groupWrap.classList.add("collapsed");
const gh = document.createElement("div");
gh.className = "wmeRcPinsGroupHdr";
gh.dataset.groupId = gid;
const groupPins = allPins.filter(p => normalizeGroupId(p.groupId) === gid);
const topLeft = document.createElement("div");
topLeft.className = "wmeRcPinsGroupTop";
const twisty = document.createElement("div");
twisty.className = "wmeRcPinsGroupTwisty";
twisty.innerHTML = `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M7 10l5 5 5-5H7z"/></svg>`;
const gName = document.createElement("div");
gName.className = "wmeRcPinsGroupName";
gName.textContent = g.name || "Folder";
topLeft.appendChild(twisty);
const gEmoji = document.createElement("div");
gEmoji.className = "wmeRcPinsGroupEmoji";
gEmoji.textContent = (g && typeof g.emoji === "string") ? g.emoji : "";
if (gEmoji.textContent) topLeft.appendChild(gEmoji);
topLeft.appendChild(gName);
const right = document.createElement("div");
right.className = "wmeRcPinsGroupActions";
const gCount = document.createElement("div");
gCount.className = "wmeRcPinsGroupCount";
gCount.textContent = String(groupPins.length);
const mkGBtn = (icon, title, onClick) => {
const b = document.createElement("div");
b.className = "wmeRcPinsGroupBtn";
b.title = title;
b.innerHTML = `<span class="wmeRcI">${icon}</span>`;
b.addEventListener("click", (ev) => { ev.stopPropagation(); onClick(); });
return b;
};
if (gid !== "default") {
right.appendChild(mkGBtn(ICONS.edit, "Rename folder", () => {
openGroupNameModal({
title: "Rename folder",
initial: g.name || "",
initialEmoji: (g && typeof g.emoji === "string") ? g.emoji : "",
placeholder: "Name",
okText: "Save",
onSubmit: (nm, emoji) => {
const gs = loadPinGroups();
const idx = gs.findIndex(x => x && x.id === gid);
if (idx >= 0) {
gs[idx] = { ...gs[idx], name: nm, emoji: (typeof emoji === 'string') ? emoji : (gs[idx].emoji || '') };
savePinGroups(gs);
renderPinsPanel();
}
}
});
}));
right.appendChild(mkGBtn(ICONS.trash, "Remove folder", () => {
openRemoveFolderModal(gid);
}));
} else {
right.appendChild(mkGBtn(ICONS.trash, "Clear pins in default folder", () => {
try {
const gnm = String(getGroupName("default") || g?.name || "(no folder)");
const pins = loadPins();
const removed = pins.filter(p => normalizeGroupId(p.groupId) === "default").length;
if (!removed) return;
openClearDefaultFolderPinsModal({ folderName: gnm, count: removed, onConfirm: () => {
const nextPins = pins.filter(p => normalizeGroupId(p.groupId) !== "default");
savePins(nextPins);
try { renderPinsMarkers(); } catch {}
renderPinsPanel();
try { _schedulePinsPanelAvoidDock(); } catch {}
}});
} catch (e) { console.error(e); }
}));
}
right.appendChild(gCount);
gh.appendChild(topLeft);
gh.appendChild(right);
gh.addEventListener("click", () => {
if (_pinDragState && _pinDragState.id) return;
const collapsed = groupWrap.classList.toggle("collapsed");
setGroupCollapsed(gid, collapsed);
try { requestAnimationFrame(() => { try { applyPinsPanelHeightAuto(); } catch {} }); } catch { try { applyPinsPanelHeightAuto(); } catch {} }
});
const onGroupDragOver = (ev) => {
if (!_pinDragState.id) return;
ev.preventDefault();
try {
_clearPinDropMarks();
gh.classList.add("dropTarget");
_pinDragState.overGroup = gid;
} catch {}
};
gh.addEventListener("dragover", onGroupDragOver);
gh.addEventListener("dragenter", onGroupDragOver);
gh.addEventListener("dragleave", () => { try { gh.classList.remove("dropTarget"); } catch {} });
gh.addEventListener("drop", (ev) => {
if (!_pinDragState.id) return;
ev.preventDefault();
try {
gh.classList.remove("dropTarget");
const fromId = _pinDragState.id;
let nextPins = _movePinToGroupEnd(loadPins(), fromId, gid);
savePins(nextPins);
renderPinsPanel();
} catch (e) { console.error(e); }
});
const body = document.createElement("div");
body.className = "wmeRcPinsGroupBody";
body.dataset.groupId = gid;
body.addEventListener("dragover", onGroupDragOver);
body.addEventListener("dragenter", onGroupDragOver);
body.addEventListener("dragleave", () => { try { gh.classList.remove("dropTarget"); } catch {} });
body.addEventListener("drop", (ev) => {
if (!_pinDragState.id) return;
ev.preventDefault();
try {
gh.classList.remove("dropTarget");
const fromId = _pinDragState.id;
let nextPins = _movePinToGroupEnd(loadPins(), fromId, gid);
savePins(nextPins);
renderPinsPanel();
} catch (e) { console.error(e); }
});
if (groupPins.length === 0) {
const empty = document.createElement("div");
empty.className = "wmeRcPinsGroupEmpty";
empty.textContent = "Drop pins here";
body.appendChild(empty);
} else {
for (const pin of groupPins) {
body.appendChild(buildPinRow(pin, gid));
}
}
groupWrap.appendChild(gh);
groupWrap.appendChild(body);
list.appendChild(groupWrap);
}
pinsPanelEl.appendChild(hdr);
pinsPanelEl.appendChild(list);
try { requestAnimationFrame(() => { try { applyPinsPanelHeightAuto(); } catch {} }); } catch { try { applyPinsPanelHeightAuto(); } catch {} }
} else {
pinsPanelEl.appendChild(hdr);
try {
const hh = Math.round((hdr.getBoundingClientRect().height || 44) + 2);
_pinsPanelLastAutoAt = _pinsNow();
_pinsPanelSuppressROUntil = _pinsPanelLastAutoAt + 650;
_pinsPanelAutoSizing = true;
pinsPanelEl.style.height = `${hh}px`;
savePinsPanelSize({ h: hh, manual: true, u: 1 });
pinsPanelEl.dataset.manualHeight = "1";
} catch {} finally { _pinsPanelAutoSizing = false; }
}
const _hasActiveReminder = allPins.some(p => p && p.reminderAt && !p.reminderDone);
if (_hasActiveReminder) startPinsCountdownLoop(); else stopPinsCountdownLoop();
}
function stopPinsCountdownLoop() {
_pinsCountdownTicking = false;
if (pinsCountdownTimerId) {
try { clearTimeout(pinsCountdownTimerId); } catch {}
pinsCountdownTimerId = null;
}
}
function getPinCountdownText(reminderAt, nowMs) {
if (!reminderAt) return "";
const ms = Number(reminderAt) - Number(nowMs);
if (!Number.isFinite(ms)) return "";
if (ms <= 0) return "";
const totalSec = Math.max(0, Math.floor(ms / 1000));
const day = 86400;
const week = 7 * day;
const month = 30 * day;
const year = 365 * day;
const plural = (n, w) => `${n} ${w}${n === 1 ? "" : "s"}`;
if (totalSec >= year) return plural(Math.floor(totalSec / year), "year");
if (totalSec >= month) return plural(Math.floor(totalSec / month), "month");
if (totalSec >= week) return plural(Math.floor(totalSec / week), "week");
if (totalSec >= day) return plural(Math.floor(totalSec / day), "day");
const h = Math.floor(totalSec / 3600);
const m = Math.floor((totalSec % 3600) / 60);
const s = totalSec % 60;
if (h > 0) return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
function getPinCountdownExact(reminderAt, nowMs) {
if (!reminderAt) return "";
const ms = Number(reminderAt) - Number(nowMs);
if (!Number.isFinite(ms)) return "";
const totalSec = Math.max(0, Math.floor(ms / 1000));
const day = 86400;
const days = Math.floor(totalSec / day);
const rem = totalSec % day;
const h = Math.floor(rem / 3600);
const m = Math.floor((rem % 3600) / 60);
const s = rem % 60;
if (days <= 0) return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
const dword = days === 1 ? "day" : "days";
return `${days} ${dword} ${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
function getPinCountdownTextMode(reminderAt, nowMs, mode) {
if (!reminderAt) return "";
const ms = Number(reminderAt) - Number(nowMs);
if (!Number.isFinite(ms)) return "";
const totalSec = Math.max(0, Math.floor(ms / 1000));
const days = Math.floor(totalSec / 86400);
const hours = Math.floor((totalSec % 86400) / 3600);
const mins = Math.floor((totalSec % 3600) / 60);
if (mode === 1) {
const parts = [];
if (days > 0) parts.push(days + "d");
if (hours > 0 || days > 0) parts.push(hours + "h");
parts.push(mins + "m");
return parts.join(" ");
}
if (mode === 2) {
const human = getPinCountdownText(reminderAt, nowMs);
const exact = getPinCountdownExact(reminderAt, nowMs);
if (!human) return exact;
if (!exact) return human;
return human + " - " + exact;
}
return getPinCountdownText(reminderAt, nowMs);
}
function updatePinsCountdowns() {
if (!pinsPanelEl || pinsPanelEl.classList.contains("hidden")) return;
const now = Date.now();
const pins = loadPins();
let hasActive = false;
for (const p of pins) {
const id = String(p.id);
const badge = _pinCountdownEls.get(id);
if (!badge) continue;
const line = badge.parentElement;
const row = _pinRowEls.get(id);
const active = !!(p.reminderAt && !p.reminderDone);
if (row) {
if (active && !row.classList.contains("wmeRcPinRowActive")) row.classList.add("wmeRcPinRowActive");
if (!active && row.classList.contains("wmeRcPinRowActive")) row.classList.remove("wmeRcPinRowActive");
}
if (!p.reminderAt || p.reminderDone) {
if (line) line.style.display = "none";
badge.textContent = "";
continue;
}
hasActive = true;
if (line) line.style.display = "";
const ms = Number(p.reminderAt) - now;
if (!Number.isFinite(ms)) continue;
if (ms <= 0) {
badge.textContent = "00:00";
try { checkRemindersNow(); } catch {}
continue;
}
const next = getPinCountdownText(p.reminderAt, now);
if (badge.textContent !== next) badge.textContent = next;
}
if (!hasActive) stopPinsCountdownLoop();
}
function startPinsCountdownLoop() {
if (_pinsCountdownTicking) return;
_pinsCountdownTicking = true;
const tick = () => {
if (!_pinsCountdownTicking) return;
try { updatePinsCountdowns(); } catch {}
const n = Date.now();
const delay = 1000 - (n % 1000) + 8;
pinsCountdownTimerId = setTimeout(tick, delay);
};
try { updatePinsCountdowns(); } catch {}
const n0 = Date.now();
const d0 = 1000 - (n0 % 1000) + 8;
pinsCountdownTimerId = setTimeout(tick, d0);
}
function removePinNow(pinId) {
const pins = loadPins().filter((p) => p.id !== String(pinId));
savePins(pins);
renderPinsPanel();
toast("Removed pin");
}
function confirmRemovePin(pinId) {
try {
const pin = loadPins().find((p) => p.id === String(pinId));
const pinName = (pin && pin.name) ? pin.name : "this pin";
openModal({
iconSvg: ICONS.trash,
title: "Delete pin",
bodyBuilder: ({ body, close, modal }) => {
const msg = document.createElement("div");
msg.className = "wmeRcHint";
msg.textContent = `Are you sure to delete ${pinName}?`;
body.appendChild(msg);
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const btnCancel = document.createElement("div");
btnCancel.className = "wmeRcModalBtn";
btnCancel.textContent = "Cancel";
btnCancel.addEventListener("click", () => close());
const btnDel = document.createElement("div");
btnDel.className = "wmeRcModalBtn primary danger";
btnDel.textContent = "Delete";
btnDel.addEventListener("click", () => {
close();
removePinNow(pinId);
});
actions.appendChild(btnCancel);
actions.appendChild(btnDel);
body.appendChild(actions);
},
});
} catch (e) {
console.error(e);
}
}
function updatePin(pinId, patch) {
const pins = loadPins();
const i = pins.findIndex((p) => p.id === String(pinId));
if (i < 0) return;
pins[i] = { ...pins[i], ...patch };
try {
if (patch && Object.prototype.hasOwnProperty.call(patch, 'reminderAt')) {
clearFiredKeysForPin(pinId);
} else if (patch && patch.reminderDone === false) {
clearFiredKeysForPin(pinId);
}
} catch {}
savePins(pins);
try {
const p = pins.find(x => x && x.id === String(pinId));
if (p) scheduleReminderTimer(p);
else clearReminderTimer(pinId);
} catch {}
renderPinsPanel();
startReminderLoop();
}
function openRenamePinModal(pinId) {
const pin = loadPins().find((p) => p.id === String(pinId));
if (!pin) return;
openModal({
title: "Edit Pin",
iconSvg: ICONS.edit,
bodyBuilder: ({ body, close, modal }) => {
let chosenColor = normalizePinColor(pin.color);
const inp = document.createElement("input");
inp.className = "wmeRcInput";
inp.placeholder = "Name…";
inp.value = pin.name || "";
const pinNameLimitMsg = attachMaxLen(inp, 28);
const hint = document.createElement("div");
hint.className = "wmeRcHint";
hint.textContent = "Tip: keep it short so it fits nicely.";
const colorLbl = document.createElement("div");
colorLbl.className = "wmeRcHint";
colorLbl.textContent = "Marker color";
const colorRow = document.createElement("div");
colorRow.className = "wmeRcColorRow";
try { colorRow.style.setProperty("--pinC", chosenColor); } catch {}
let customColors = loadCustomPinColors();
let editCustomIndex = null;
const swatches = [];
function setColor(hex){
chosenColor = normalizePinColor(hex);
for (const s of swatches) s.classList.toggle("sel", s.dataset.c === chosenColor);
try { colorRow.style.setProperty("--pinC", chosenColor); } catch {}
}
function parseAnyColorLocal(s){
try{
const str = String(s || "").trim();
if (!str) return null;
if (str.startsWith("#")) return normalizePinColor(str);
const m = str.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*([0-9.]+)\s*)?\)$/i);
if (m){
const r = Math.max(0, Math.min(255, Number(m[1])));
const g = Math.max(0, Math.min(255, Number(m[2])));
const b = Math.max(0, Math.min(255, Number(m[3])));
return normalizePinColor(`#${((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1)}`);
}
return null;
}catch{ return null; }
}
function hexToRgbLocal(hex){
try{
const h = normalizePinColor(hex);
const x = h.replace("#","");
const r = parseInt(x.slice(0,2),16);
const g = parseInt(x.slice(2,4),16);
const b = parseInt(x.slice(4,6),16);
return { r,g,b };
}catch{ return { r:0,g:0,b:0 }; }
}
const pickTextOnLocal = (hex) => {
try{
const h = normalizePinColor(hex) || "#000000";
const x = h.replace("#","");
const r = parseInt(x.slice(0,2),16);
const g = parseInt(x.slice(2,4),16);
const b = parseInt(x.slice(4,6),16);
const srgb = [r,g,b].map(v => {
const c = v/255;
return c <= 0.03928 ? c/12.92 : ((c+0.055)/1.055)**2.4;
});
const L = 0.2126*srgb[0] + 0.7152*srgb[1] + 0.0722*srgb[2];
return (L > 0.62) ? "#111" : "#fff";
}catch{ return "#111"; }
};
for (const c of PIN_COLOR_PRESETS){
const sw = document.createElement("div");
sw.className = "wmeRcColorSwatch";
sw.style.setProperty("--c", c);
sw.dataset.c = normalizePinColor(c);
sw.title = c;
sw.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); setColor(c); });
swatches.push(sw);
colorRow.appendChild(sw);
}
const plusBtn = document.createElement("div");
plusBtn.className = "wmeRcColorPlus";
plusBtn.textContent = "+";
plusBtn.title = "Custom color";
colorRow.appendChild(plusBtn);
let colorPop = null;
function positionPop(anchorEl){
try{
const r = anchorEl.getBoundingClientRect();
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
const w = 286;
let left = r.right + 10;
if (left + w > vw - 10) left = r.left - w - 10;
left = Math.min(vw - w - 10, Math.max(10, left));
let top = r.bottom + 8;
const maxH = 320;
if (top + maxH + 10 > vh) top = Math.max(10, r.top - 8 - maxH);
colorPop.style.left = left + "px";
colorPop.style.top = top + "px";
}catch{}
}
function closePop(){
try { if (colorPop) colorPop.remove(); } catch {}
colorPop = null;
try { document.removeEventListener("pointerdown", onOutside, true); } catch {}
}
function onOutside(e){
try{
if (!colorPop) return;
if (colorPop.contains(e.target)) return;
if (plusBtn.contains(e.target)) return;
closePop();
}catch{}
}
function openPop({ initial, editIndex, anchorEl }){
closePop();
colorPop = document.createElement("div");
colorPop.className = "wmeRcColorPop";
let cur = normalizePinColor(initial || chosenColor || "#ff8a00") || "#ff8a00";
const topRow = document.createElement('div');
topRow.className = 'wmeRcColorTopRow';
const swBig = document.createElement('div');
swBig.className = 'wmeRcColorSwatchBig';
const stats = document.createElement('div');
stats.className = 'wmeRcColorStats';
const stat1 = document.createElement('div');
stat1.className = 'wmeRcColorStatLine';
stat1.innerHTML = `<span><span class="wmeRcColorStatKey">HEX</span><span class="wmeRcColorStatVal" data-k="hex">#000000</span></span>
<span><span class="wmeRcColorStatKey">RGB</span><span class="wmeRcColorStatVal" data-k="rgb">0, 0, 0</span></span>`;
const stat2 = document.createElement('div');
stat2.className = 'wmeRcColorStatLine';
stat2.innerHTML = `<span><span class="wmeRcColorStatKey">HSL</span><span class="wmeRcColorStatVal" data-k="hsl">0, 0%, 0%</span></span>`;
stats.appendChild(stat1);
stats.appendChild(stat2);
topRow.appendChild(swBig);
topRow.appendChild(stats);
colorPop.appendChild(topRow);
const pickerRow = document.createElement('div');
pickerRow.className = 'wmeRcPickerRow';
const sv = document.createElement('div');
sv.className = 'wmeRcPickerSV';
const svDot = document.createElement('div');
svDot.className = 'wmeRcPickerSVDot';
sv.appendChild(svDot);
const hue = document.createElement('div');
hue.className = 'wmeRcPickerHue';
const hueThumb = document.createElement('div');
hueThumb.className = 'wmeRcPickerHueThumb';
hue.appendChild(hueThumb);
const fields = document.createElement('div');
fields.className = 'wmeRcPickerFields';
const mkRow = (lab, input) => {
const row = document.createElement('div');
row.className = 'wmeRcPickerFieldRow';
const l = document.createElement('label');
l.textContent = lab;
row.appendChild(l);
row.appendChild(input);
return row;
};
const hexField = document.createElement('input');
hexField.className = 'wmeRcPickerField';
hexField.placeholder = '#RRGGBB';
hexField.autocomplete = 'off';
hexField.spellcheck = false;
const rField = document.createElement('input');
rField.className = 'wmeRcPickerField';
rField.placeholder = '0';
rField.inputMode = 'numeric';
rField.autocomplete = 'off';
const gField = document.createElement('input');
gField.className = 'wmeRcPickerField';
gField.placeholder = '0';
gField.inputMode = 'numeric';
gField.autocomplete = 'off';
const bField = document.createElement('input');
bField.className = 'wmeRcPickerField';
bField.placeholder = '0';
bField.inputMode = 'numeric';
bField.autocomplete = 'off';
fields.appendChild(mkRow('HEX', hexField));
fields.appendChild(mkRow('R', rField));
fields.appendChild(mkRow('G', gField));
fields.appendChild(mkRow('B', bField));
pickerRow.appendChild(sv);
pickerRow.appendChild(hue);
pickerRow.appendChild(fields);
colorPop.appendChild(pickerRow);
const actions = document.createElement('div');
actions.className = 'wmeRcColorActions';
const btnSavePreset = document.createElement('div');
btnSavePreset.className = 'wmeRcColorSavePreset';
btnSavePreset.textContent = 'Save preset';
const btnDelete = document.createElement('button');
btnDelete.className = 'wmeRcColorDelete';
btnDelete.type = 'button';
btnDelete.textContent = 'Delete';
const btnDone = document.createElement('button');
btnDone.className = 'wmeRcColorDone';
btnDone.type = 'button';
btnDone.textContent = 'Done';
const isPresetEdit = Number.isFinite(editIndex) && editIndex != null;
if (isPresetEdit){
btnSavePreset.style.display = 'none';
btnDone.textContent = 'Save';
actions.appendChild(btnDelete);
}
actions.appendChild(btnSavePreset);
actions.appendChild(btnDone);
colorPop.appendChild(actions);
const clamp01 = (x) => Math.max(0, Math.min(1, x));
const clamp255 = (x) => {
const n = Number(String(x || '').replace(/[^\d]/g, ''));
if (!Number.isFinite(n)) return null;
return Math.max(0, Math.min(255, Math.round(n)));
};
const hexToRgb = (hex) => {
try{
const h = normalizePinColor(hex);
const x = h.replace('#','');
return { r: parseInt(x.slice(0,2),16), g: parseInt(x.slice(2,4),16), b: parseInt(x.slice(4,6),16) };
}catch{ return { r:0,g:0,b:0 }; }
};
const rgbToHex = (r,g,b) => {
const n = (1<<24) + ((r&255)<<16) + ((g&255)<<8) + (b&255);
return '#' + n.toString(16).slice(1);
};
const rgbToHsl = (r,g,b) => {
r/=255; g/=255; b/=255;
const max = Math.max(r,g,b), min = Math.min(r,g,b);
let h=0,s=0,l=(max+min)/2;
if (max!==min){
const d = max-min;
s = l>0.5 ? d/(2-max-min) : d/(max+min);
switch(max){
case r: h = (g-b)/d + (g<b?6:0); break;
case g: h = (b-r)/d + 2; break;
case b: h = (r-g)/d + 4; break;
}
h/=6;
}
return { h: Math.round(h*360), s: Math.round(s*100), l: Math.round(l*100) };
};
const rgbToHsv = (r,g,b) => {
r/=255; g/=255; b/=255;
const max = Math.max(r,g,b), min = Math.min(r,g,b);
const d = max-min;
let h=0;
if (d!==0){
if (max===r) h = ((g-b)/d) % 6;
else if (max===g) h = (b-r)/d + 2;
else h = (r-g)/d + 4;
h = Math.round(h*60);
if (h<0) h += 360;
}
const s = max===0 ? 0 : d/max;
const v = max;
return { h, s, v };
};
const hsvToRgb = (h,s,v) => {
h = ((h%360)+360)%360;
const c = v*s;
const x = c*(1-Math.abs(((h/60)%2)-1));
const m = v-c;
let rp=0,gp=0,bp=0;
if (h<60){rp=c;gp=x;bp=0;}
else if (h<120){rp=x;gp=c;bp=0;}
else if (h<180){rp=0;gp=c;bp=x;}
else if (h<240){rp=0;gp=x;bp=c;}
else if (h<300){rp=x;gp=0;bp=c;}
else {rp=c;gp=0;bp=x;}
return { r: Math.round((rp+m)*255), g: Math.round((gp+m)*255), b: Math.round((bp+m)*255) };
};
let hsv = (() => {
const rgb = hexToRgb(cur);
return rgbToHsv(rgb.r, rgb.g, rgb.b);
})();
const setCur = (hex) => {
const parsed = parseAnyColorLocal(hex);
if (!parsed) return false;
cur = parsed;
const rgb = hexToRgb(cur);
hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
return true;
};
const renderStats = () => {
const rgb = hexToRgb(cur);
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
try{ swBig.style.setProperty('--c', cur); }catch{}
try{ swBig.style.background = cur; }catch{}
try{ colorPop.style.setProperty('--hue', `hsl(${hsv.h} 100% 50%)`); }catch{}
try{
const br = (rgb.r*299 + rgb.g*587 + rgb.b*114) / 1000;
btnDone.style.background = cur;
btnDone.style.color = (br > 160) ? 'rgba(0,0,0,.82)' : '#fff';
}catch{}
const hexEl = colorPop.querySelector('[data-k="hex"]');
const rgbEl = colorPop.querySelector('[data-k="rgb"]');
const hslEl = colorPop.querySelector('[data-k="hsl"]');
if (hexEl) hexEl.textContent = cur.toUpperCase();
if (rgbEl) rgbEl.textContent = `${rgb.r}, ${rgb.g}, ${rgb.b}`;
if (hslEl) hslEl.textContent = `${hsl.h}, ${hsl.s}%, ${hsl.l}%`;
hexField.value = cur.toUpperCase();
rField.value = String(rgb.r);
gField.value = String(rgb.g);
bField.value = String(rgb.b);
const rect = sv.getBoundingClientRect();
const x = hsv.s * rect.width;
const y = (1 - hsv.v) * rect.height;
svDot.style.left = x + 'px';
svDot.style.top = y + 'px';
const hrect = hue.getBoundingClientRect();
hueThumb.style.top = (hsv.h / 360) * hrect.height + 'px';
};
const setFromSVEvent = (ev) => {
const r = sv.getBoundingClientRect();
const x = clamp01((ev.clientX - r.left) / r.width);
const y = clamp01((ev.clientY - r.top) / r.height);
hsv.s = x;
hsv.v = 1 - y;
const rgb = hsvToRgb(hsv.h, hsv.s, hsv.v);
cur = rgbToHex(rgb.r, rgb.g, rgb.b);
renderStats();
};
const setFromHueEvent = (ev) => {
const r = hue.getBoundingClientRect();
const y = clamp01((ev.clientY - r.top) / r.height);
hsv.h = Math.round(y * 360);
const rgb = hsvToRgb(hsv.h, hsv.s, hsv.v);
cur = rgbToHex(rgb.r, rgb.g, rgb.b);
renderStats();
};
const bindDrag = (el, onMove) => {
const onDown = (e) => {
e.preventDefault(); e.stopPropagation();
try{ el.setPointerCapture(e.pointerId); }catch{}
onMove(e);
const mm = (ev) => onMove(ev);
const uu = () => {
try{ el.releasePointerCapture(e.pointerId); }catch{}
el.removeEventListener('pointermove', mm);
el.removeEventListener('pointerup', uu);
el.removeEventListener('pointercancel', uu);
};
el.addEventListener('pointermove', mm);
el.addEventListener('pointerup', uu);
el.addEventListener('pointercancel', uu);
};
el.addEventListener('pointerdown', onDown);
};
bindDrag(sv, setFromSVEvent);
bindDrag(hue, setFromHueEvent);
hexField.addEventListener('input', () => {
const v = String(hexField.value || '').trim();
if (setCur(v)) renderStats();
});
const applyRgb = () => {
const r = clamp255(rField.value);
const g = clamp255(gField.value);
const b = clamp255(bField.value);
if (r == null || g == null || b == null) return;
setCur(rgbToHex(r,g,b));
renderStats();
};
rField.addEventListener('input', applyRgb);
gField.addEventListener('input', applyRgb);
bField.addEventListener('input', applyRgb);
btnSavePreset.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
const norm = normalizePinColor(cur);
if (!norm) return;
const arr = loadCustomPinColors();
const already = arr.findIndex(c => normalizePinColor(c) === norm);
if (already !== -1 && !(Number.isFinite(editIndex) && editIndex != null && already === editIndex)) {
try { toast("That preset already exists."); } catch {}
return;
}
if (Number.isFinite(editIndex) && editIndex != null && editIndex >= 0 && editIndex < arr.length){
arr[editIndex] = norm;
} else if (arr.length < 4){
arr.push(norm);
} else {
arr[arr.length - 1] = norm;
}
saveCustomPinColors(arr);
customColors = loadCustomPinColors();
renderCustomSwatches();
renderStats();
});
btnDelete.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
if (!(Number.isFinite(editIndex) && editIndex != null)) { closePop(); return; }
const arr = loadCustomPinColors();
if (editIndex >= 0 && editIndex < arr.length){
arr.splice(editIndex, 1);
saveCustomPinColors(arr);
customColors = loadCustomPinColors();
renderCustomSwatches();
renderStats();
}
closePop();
});
btnDone.addEventListener('click', (e) => {
e.preventDefault(); e.stopPropagation();
const isEditing = (Number.isFinite(Number(editIndex)) && editIndex != null);
if (isEditing) {
const norm = normalizePinColor(cur);
if (!norm) return;
let arr = loadCustomPinColors();
const idx = Number(editIndex);
const already = arr.findIndex(c => normalizePinColor(c) === norm);
if (already !== -1 && already !== idx) {
try { toast("That preset already exists."); } catch {}
return;
}
if (idx >= 0 && idx < arr.length) {
arr[idx] = norm;
saveCustomPinColors(arr);
customColors = loadCustomPinColors();
renderCustomSwatches();
}
try { setColor(norm); } catch {}
closePop();
return;
}
try { setColor(cur); } catch {}
closePop();
});
renderStats();
document.body.appendChild(colorPop);
positionPop(anchorEl || plusBtn);
document.addEventListener('pointerdown', onOutside, true);
}
let swatchEditTipEl = null;
let swatchEditTipHideT = null;
let swatchEditAnchor = null;
let swatchEditIndex = null;
function ensureSwatchEditTip(){
if (swatchEditTipEl) return;
swatchEditTipEl = document.createElement("div");
swatchEditTipEl.className = "wmeRcSwatchEditTip";
swatchEditTipEl.style.display = "none";
swatchEditTipEl.innerHTML = `
<button type="button" class="wmeRcSwatchEditTipBtn" data-act="edit" aria-label="Edit color">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 20h9"/>
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/>
</svg>
</button>
<button type="button" class="wmeRcSwatchEditTipBtn" data-act="del" aria-label="Remove color">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 6h18"/>
<path d="M8 6V4h8v2"/>
<path d="M19 6l-1 14H6L5 6"/>
<path d="M10 11v6"/>
<path d="M14 11v6"/>
</svg>
</button>
`;
swatchEditTipEl.addEventListener("pointerenter", () => {
if (swatchEditTipHideT){ clearTimeout(swatchEditTipHideT); swatchEditTipHideT = null; }
});
swatchEditTipEl.addEventListener("pointerleave", () => hideSwatchEditTip());
swatchEditTipEl.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
const btn = e.target && e.target.closest ? e.target.closest(".wmeRcSwatchEditTipBtn") : null;
const act = btn && btn.dataset ? btn.dataset.act : null;
if (!act) return;
if (!swatchEditAnchor || !Number.isFinite(swatchEditIndex)) return;
const arr = loadCustomPinColors();
if (act === "edit"){
const c = arr[swatchEditIndex];
if (!c) return;
openPop({ initial: c, editIndex: swatchEditIndex, anchorEl: swatchEditAnchor });
return;
}
if (act === "del"){
if (swatchEditIndex < 0 || swatchEditIndex >= arr.length) return;
arr.splice(swatchEditIndex, 1);
saveCustomPinColors(arr);
customColors = loadCustomPinColors();
renderCustomSwatches();
hideSwatchEditTip(true);
}
});
document.body.appendChild(swatchEditTipEl);
}
function showSwatchEditTip(anchorEl, idx){
ensureSwatchEditTip();
swatchEditAnchor = anchorEl;
swatchEditIndex = idx;
if (swatchEditTipHideT){ clearTimeout(swatchEditTipHideT); swatchEditTipHideT = null; }
const r = anchorEl.getBoundingClientRect();
const vw = window.innerWidth || document.documentElement.clientWidth || 9999;
swatchEditTipEl.style.display = "inline-flex";
swatchEditTipEl.style.visibility = "hidden";
swatchEditTipEl.style.left = "0px";
swatchEditTipEl.style.top = "0px";
const tipW = Math.max(1, swatchEditTipEl.getBoundingClientRect().width || swatchEditTipEl.offsetWidth || 1);
let left = r.left + (r.width / 2) - (tipW / 2);
left = Math.max(8, Math.min(vw - tipW - 8, left));
const top = r.bottom + 6;
swatchEditTipEl.style.left = left + "px";
swatchEditTipEl.style.top = top + "px";
swatchEditTipEl.style.visibility = "visible";
}
function hideSwatchEditTip(immediate){
if (!swatchEditTipEl) return;
const doHide = () => { try{ swatchEditTipEl.style.display = "none"; }catch{} };
if (immediate) return doHide();
if (swatchEditTipHideT) clearTimeout(swatchEditTipHideT);
swatchEditTipHideT = setTimeout(doHide, 220);
}
function renderCustomSwatches(){
try{
const oldSep = colorRow.querySelector(".wmeRcColorSep");
if (oldSep) oldSep.remove();
}catch{}
for (let i = swatches.length - 1; i >= 0; i--){
const s = swatches[i];
if (s && s.dataset && s.dataset.customIndex != null){
try { s.remove(); } catch {}
swatches.splice(i, 1);
}
}
if (customColors && customColors.length){
const sep = document.createElement("div");
sep.className = "wmeRcColorSep";
sep.textContent = "|";
colorRow.insertBefore(sep, plusBtn);
}
for (let i = 0; i < customColors.length; i++){
const c = customColors[i];
const sw = document.createElement("div");
sw.className = "wmeRcColorSwatch custom";
sw.style.setProperty("--c", c);
sw.dataset.c = normalizePinColor(c);
sw.dataset.customIndex = String(i);
sw.title = c;
sw.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
setColor(c);
editCustomIndex = i;
hideSwatchEditTip(true);
closePop();
});
sw.addEventListener("pointerenter", () => showSwatchEditTip(sw, i));
sw.addEventListener("pointerleave", () => hideSwatchEditTip());
colorRow.insertBefore(sw, plusBtn);
swatches.push(sw);
}
for (const s of swatches) s.classList.toggle("sel", s.dataset.c === chosenColor);
}
plusBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
editCustomIndex = null;
openPop({ initial: chosenColor, editIndex: null, anchorEl: plusBtn });
});
renderCustomSwatches();
setColor(chosenColor);
let chosenGroupId = normalizeGroupId(pin.groupId);
const groupLbl = document.createElement("div");
groupLbl.className = "wmeRcHint";
groupLbl.textContent = "Folder";
const groupPick = document.createElement("div");
groupPick.className = "wmeRcSoundPick";
groupPick.tabIndex = 0;
const groupBtn = document.createElement("div");
groupBtn.className = "wmeRcSoundBtn";
const groupBtnLabel = document.createElement("div");
groupBtnLabel.className = "wmeRcSoundBtnLabel";
const groupCaret = document.createElement("div");
groupCaret.className = "wmeRcSoundCaret";
groupCaret.innerHTML = ICONS.chevDown || "▾";
groupBtn.appendChild(groupBtnLabel);
groupBtn.appendChild(groupCaret);
groupPick.appendChild(groupBtn);
const groupMenu = document.createElement("div");
groupMenu.className = "wmeRcSoundMenu wmeRcSoundMenuPortal";
groupMenu.style.display = "none";
try { document.body.appendChild(groupMenu); } catch {}
const getGroupLabel = (id) => {
try {
const gg = loadPinGroups().find(x => x && x.id === id);
return gg ? (gg.name || "(no folder)") : "(no folder)";
} catch { return "(no folder)"; }
};
const positionGroupMenu = () => {
try {
const r = groupBtn.getBoundingClientRect();
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
const w = Math.max(260, Math.min(r.width || 320, vw - 16));
let left = Math.max(8, Math.min(r.left, vw - w - 8));
let top = (r.bottom || 0) + 6;
const maxH = 240;
if (top + maxH + 8 > vh) top = Math.max(8, (r.top || 0) - 6 - maxH);
groupMenu.style.left = left + "px";
groupMenu.style.top = top + "px";
groupMenu.style.width = w + "px";
const avail = Math.max(120, vh - top - 10);
groupMenu.style.maxHeight = Math.min(maxH, avail) + "px";
} catch {}
};
const toggleGroupMenu = (open) => {
const isOpen = groupPick.getAttribute("data-open") === "1";
const next = (typeof open === "boolean") ? open : !isOpen;
groupPick.setAttribute("data-open", next ? "1" : "0");
try {
if (next) {
positionGroupMenu();
groupMenu.style.display = "block";
} else {
groupMenu.style.display = "none";
}
} catch {}
};
const rebuildGroupMenu = () => {
groupMenu.innerHTML = "";
const gs = loadPinGroups();
for (const g of gs) {
const item = document.createElement("div");
item.className = "wmeRcSoundItem";
item.textContent = g.name;
item.dataset.groupId = g.id;
item.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
chosenGroupId = normalizeGroupId(g.id);
groupBtnLabel.textContent = getGroupLabel(chosenGroupId);
toggleGroupMenu(false);
});
groupMenu.appendChild(item);
}
const newItem = document.createElement("div");
newItem.className = "wmeRcSoundItem";
newItem.textContent = "+ New Folder";
newItem.dataset.groupId = "__new__";
newItem.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
toggleGroupMenu(false);
openGroupNameModal({
title: "New folder",
placeholder: "Name",
okText: "Create",
onCancel: () => {},
onSubmit: (nm, emoji) => {
const id = createPinGroup(nm, emoji);
chosenGroupId = id;
groupBtnLabel.textContent = getGroupLabel(chosenGroupId);
rebuildGroupMenu();
}
});
});
groupMenu.appendChild(newItem);
};
groupBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
toggleGroupMenu();
});
const onDocDown = (e) => {
try {
if (!groupPick.contains(e.target) && !groupMenu.contains(e.target)) {
groupPick.setAttribute("data-open", "0");
groupMenu.style.display = "none";
}
} catch {}
};
document.addEventListener("pointerdown", onDocDown, true);
try { window.addEventListener("resize", positionGroupMenu, true); } catch {}
try { window.addEventListener("scroll", positionGroupMenu, true); } catch {}
const cleanupGroupPick = () => {
try { document.removeEventListener("pointerdown", onDocDown, true); } catch {}
try { window.removeEventListener("resize", positionGroupMenu, true); } catch {}
try { window.removeEventListener("scroll", positionGroupMenu, true); } catch {}
try { groupMenu.remove(); } catch {}
};
try {
const mo = new MutationObserver(() => {
try {
if (!document.body.contains(modal)) {
cleanupGroupPick();
mo.disconnect();
}
} catch {}
});
mo.observe(document.body, { childList: true });
} catch {}
rebuildGroupMenu();
groupBtnLabel.textContent = getGroupLabel(chosenGroupId);
setColor(chosenColor);
const visWrap = document.createElement("div");
visWrap.className = "wmeRcHint";
visWrap.innerHTML = `
<label class="wmeRcInlineToggle">
<input type="checkbox" class="wmeRcVisChk">
<span class="wmeRcSwitchTrack"><span class="wmeRcSwitchThumb"></span></span>
<span class="wmeRcSwitchLabel">Show on map</span>
</label>
`;
const visChk = visWrap.querySelector("input");
visChk.checked = !(pin.hideOnMap === true);
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const btnCancel = document.createElement("div");
btnCancel.className = "wmeRcModalBtn";
btnCancel.textContent = "Cancel";
btnCancel.addEventListener("click", () => { try { if (typeof cleanupSoundPick === "function") cleanupSoundPick(); } catch {} close(); });
const btnSave = document.createElement("div");
btnSave.className = "wmeRcModalBtn primary";
btnSave.textContent = "Save";
btnSave.addEventListener("click", () => {
const v = validatePinName(inp.value, "Pinned place");
if (!v.ok) { toast(v.msg); try { inp.focus(); inp.select(); } catch {} return; }
const name = v.value;
updatePin(pinId, { name, color: chosenColor, groupId: chosenGroupId, hideOnMap: !((visChk && visChk.checked)) });
try { setLastGroup(chosenGroupId); } catch {}
toast("Pin renamed");
close();
});
actions.appendChild(btnCancel);
actions.appendChild(btnSave);
body.appendChild(pinNameLimitMsg ? pinNameLimitMsg.wrap : inp);
if (pinNameLimitMsg && pinNameLimitMsg.msg) body.appendChild(pinNameLimitMsg.msg);
body.appendChild(colorLbl);
body.appendChild(colorRow);
body.appendChild(groupLbl);
body.appendChild(groupPick);
body.appendChild(visWrap);
body.appendChild(actions);
setTimeout(() => { try { inp.focus(); inp.select(); } catch {} }, 50);
},
});
}
function getExternalNotifyCfg() {
const key = `${SCRIPT_ID}:pinsExternalNotify:v1`;
try {
const raw = localStorage.getItem(key);
const obj = JSON.parse(raw || "{}") || {};
return {
enabled: obj.enabled === true,
url: typeof obj.url === "string" ? obj.url.trim() : "",
};
} catch {
return { enabled: false, url: "" };
}
}
function setExternalNotifyCfg(cfg) {
const key = `${SCRIPT_ID}:pinsExternalNotify:v1`;
try { localStorage.setItem(key, JSON.stringify(cfg || {})); } catch {}
}
const REMINDER_SOUND_OPTIONS = [
{ id: "mute", label: "(no sound)" },
{ id: "bell", label: "Classic bell" },
{ id: "softBell", label: "Soft bell" },
{ id: "church", label: "Church bell" },
{ id: "chime", label: "Chime" },
{ id: "doubleDing", label: "Double ding" },
{ id: "digital", label: "Digital beep" },
{ id: "retro", label: "Retro beep" },
{ id: "alarm", label: "Alarm" },
{ id: "alarmFast", label: "Alarm fast" },
{ id: "alarmPulse", label: "Alarm pulse" },
{ id: "buzzer", label: "Buzzer" },
{ id: "radar", label: "Radar ping" },
{ id: "siren", label: "Soft siren" },
{ id: "gong", label: "Gong" },
{ id: "glass", label: "Glass ping" },
{ id: "wood", label: "Wood knock" },
];
const REMINDER_NOTICE_POSITION_OPTIONS = [
{ id: "right", label: "Right" },
{ id: "left", label: "Left" },
];
function getReminderNoticePosition() {
const key = `${SCRIPT_ID}:pinsNoticePos:v1`;
try {
const v = String(localStorage.getItem(key) || "").trim().toLowerCase();
if (REMINDER_NOTICE_POSITION_OPTIONS.some(o => o.id === v)) return v;
} catch {}
return "right";
}
function setReminderNoticePosition(pos) {
const key = `${SCRIPT_ID}:pinsNoticePos:v1`;
const v = REMINDER_NOTICE_POSITION_OPTIONS.some(o => o.id === String(pos).toLowerCase())
? String(pos).toLowerCase()
: "right";
try { localStorage.setItem(key, v); } catch {}
try { applyReminderNoticePosition(); } catch {}
}
function getLeftSidebarInsetPx() {
try {
const selectors = [
"#side-panel", "#sidepanel", "#sidePanel", "#sidebar",
".side-panel", ".sidepanel", ".sidebar",
".wz-sidebar", ".wz-side-panel", ".wz-sidepanel",
".wme-sidebar", ".wme-sidepanel", ".wme-side-panel",
"aside"
];
let inset = 0;
for (const sel of selectors) {
const els = document.querySelectorAll(sel);
for (const el of els) {
if (!el || !(el instanceof Element)) continue;
const cs = window.getComputedStyle(el);
if (!cs || cs.display === "none" || cs.visibility === "hidden" || Number(cs.opacity || "1") < 0.05) continue;
const r = el.getBoundingClientRect();
if (!r || r.width < 120 || r.height < 220) continue;
if (r.left > 80) continue;
inset = Math.max(inset, r.right || 0);
}
}
inset = Math.max(0, inset);
if (inset > 0) inset = Math.min(inset, Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0) - 80);
return inset;
} catch {
return 0;
}
}
function applyReminderNoticePosition() {
try {
const stack = document.getElementById("wmeRcNoticeStack");
if (!stack) return;
const pos = getReminderNoticePosition();
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const inset = getLeftSidebarInsetPx();
stack.style.bottom = "118px";
stack.style.transform = "";
if (pos === "left") {
stack.style.left = Math.round(inset + 12) + "px";
stack.style.right = "";
stack.style.alignItems = "flex-start";
} else if (pos === "middle") {
const desired = Math.round((vw - 440) / 2);
const left = Math.max(Math.round(inset + 12), isFinite(desired) ? desired : Math.round(vw * 0.5 - 220));
stack.style.left = left + "px";
stack.style.right = "";
stack.style.alignItems = "flex-start";
} else {
stack.style.right = "96px";
stack.style.left = "";
stack.style.alignItems = "flex-end";
}
} catch {}
}
try {
window.addEventListener("resize", () => { try { applyReminderNoticePosition(); } catch {} });
} catch {}
function getReminderSoundId() {
const key = `${SCRIPT_ID}:pinsReminderSound:v1`;
try {
const v = String(localStorage.getItem(key) || "").trim();
if (REMINDER_SOUND_OPTIONS.some(o => o.id === v)) return v;
} catch {}
return "bell";
}
function setReminderSoundId(id) {
const key = `${SCRIPT_ID}:pinsReminderSound:v1`;
const v = REMINDER_SOUND_OPTIONS.some(o => o.id === id) ? id : "bell";
try { localStorage.setItem(key, v); } catch {}
}
async function sendExternalReminderWebhook(pin) {
try {
const cfg = getExternalNotifyCfg();
if (!cfg || !cfg.enabled || !cfg.url) return;
const note = (pin && typeof pin.reminderNote === "string") ? pin.reminderNote.trim() : "";
const payload = {
type: "wme_pin_reminder",
title: "WME Pin Reminder",
message: note ? `Reminder: ${pin?.name || "Pin"} — ${note}` : `Reminder: ${pin?.name || "Pin"}`,
pin: {
id: String(pin?.id || ""),
name: String(pin?.name || ""),
lat: Number(pin?.lat),
lon: Number(pin?.lon),
groupId: String(pin?.groupId || "default"),
when: Number(pin?.reminderAt) || Date.now(),
},
ts: Date.now(),
};
await fetch(cfg.url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
mode: "cors",
credentials: "omit",
}).catch(() => {});
} catch {}
}
function openPinsSettingsModal() {
openModal({
title: "Pin settings",
iconSvg: ICONS.gear,
bodyBuilder: ({ body, close, modal }) => {
const visWrap = document.createElement("div");
visWrap.className = "wmeRcHint";
visWrap.innerHTML = `
<label class="wmeRcInlineToggle">
<input type="checkbox" class="wmeRcVisAllChk">
<span class="wmeRcSwitchTrack"><span class="wmeRcSwitchThumb"></span></span>
<span class="wmeRcSwitchLabel">Pins on map</span>
</label>
`;
const visChk = visWrap.querySelector("input");
visChk.checked = !!getPinsLayerVisible();
visChk.addEventListener("change", () => {
setPinsLayerVisible(!!visChk.checked);
try { if (namesChk) namesChk.disabled = !getPinsLayerVisible(); } catch {}
try {
const zi = modal?.querySelector?.(".wmeRcPinZoomPick");
if (zi) {
const dis = !getPinsLayerVisible() || !getPinsShowNamesOnMap();
try { zi.style.opacity = dis ? ".55" : "1"; } catch {}
try { zi.style.pointerEvents = dis ? "none" : "auto"; } catch {}
try { zi.tabIndex = dis ? -1 : 0; } catch {}
}
} catch {}
try { renderPinsMarkers(); } catch {}
});
const namesWrap = document.createElement("div");
namesWrap.className = "wmeRcHint";
namesWrap.innerHTML = `
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;">
<label class="wmeRcInlineToggle" style="margin:0;">
<input type="checkbox" class="wmeRcPinNamesChk">
<span class="wmeRcSwitchTrack"><span class="wmeRcSwitchThumb"></span></span>
<span class="wmeRcSwitchLabel">Show pin names on map</span>
</label>
<div style="display:flex;align-items:center;justify-content:flex-end;gap:10px;min-width:220px;">
<span style="opacity:.85;white-space:nowrap;">Zoom visibility</span>
<div class="wmeRcSoundPick wmeRcPinZoomPick" tabindex="0" style="width:120px;max-width:120px;">
<div class="wmeRcSoundBtn">
<div class="wmeRcSoundBtnLabel wmeRcPinZoomLbl"></div>
<div class="wmeRcSoundCaret wmeRcPinZoomCaret"></div>
</div>
</div>
</div>
</div>
`;
const namesChk = namesWrap.querySelector(".wmeRcPinNamesChk");
const zoomPick = namesWrap.querySelector(".wmeRcPinZoomPick");
const zoomBtn = namesWrap.querySelector(".wmeRcPinZoomPick .wmeRcSoundBtn");
const zoomLbl = namesWrap.querySelector(".wmeRcPinZoomLbl");
const zoomCaret = namesWrap.querySelector(".wmeRcPinZoomCaret");
try { zoomCaret.innerHTML = ICONS.chevDown; } catch { try { zoomCaret.textContent = "▾"; } catch {} }
const clampZoom = (n) => Math.max(4, Math.min(22, Number(n) || 9));
let zoomVal = clampZoom(getPinsNamesMinZoom());
const applyZoomLabel = () => { try { zoomLbl.textContent = String(zoomVal); } catch {} };
const isPinsLayerOn = () => !!getPinsLayerVisible();
const updateZoomDisabled = () => {
const dis = !isPinsLayerOn() || !namesChk.checked;
try { zoomPick.style.opacity = dis ? ".55" : "1"; } catch {}
try { zoomPick.style.pointerEvents = dis ? "none" : "auto"; } catch {}
try { zoomPick.tabIndex = dis ? -1 : 0; } catch {}
};
try { namesChk.checked = !!getPinsShowNamesOnMap(); } catch { namesChk.checked = false; }
try { namesChk.disabled = !isPinsLayerOn(); } catch {}
applyZoomLabel();
updateZoomDisabled();
const zoomMenu = document.createElement("div");
zoomMenu.className = "wmeRcSoundMenu wmeRcSoundMenuPortal";
zoomMenu.style.display = "none";
try { document.body.appendChild(zoomMenu); } catch {}
const positionZoomMenu = () => {
try {
const r = zoomBtn.getBoundingClientRect();
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
const w = Math.max(120, Math.min((r.width || 120), 220, vw - 16));
let left = Math.max(8, Math.min(r.left, vw - w - 8));
let top = (r.bottom || 0) + 6;
const maxH = 240;
if (top + maxH + 8 > vh) top = Math.max(8, (r.top || 0) - 6 - maxH);
zoomMenu.style.left = left + "px";
zoomMenu.style.top = top + "px";
zoomMenu.style.width = w + "px";
const avail = Math.max(120, vh - top - 10);
zoomMenu.style.maxHeight = Math.min(maxH, avail) + "px";
zoomMenu.style.overflow = "auto";
} catch {}
};
const setZoomValue = (n, save = true) => {
zoomVal = clampZoom(n);
applyZoomLabel();
if (save) { try { setPinsNamesMinZoom(zoomVal); } catch {} }
try { zoomMenu.style.display = "none"; } catch {}
try { zoomPick.setAttribute("data-open", "0"); } catch {}
};
for (let z = 4; z <= 22; z++) {
const item = document.createElement("div");
item.className = "wmeRcSoundItem";
item.textContent = String(z);
item.dataset.zoom = String(z);
item.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
setZoomValue(z, true);
});
zoomMenu.appendChild(item);
}
setZoomValue(zoomVal, false);
const toggleZoomMenu = (open) => {
const dis = !isPinsLayerOn() || !namesChk.checked;
if (dis) return;
const isOpen = zoomPick.getAttribute("data-open") === "1";
const next = (typeof open === "boolean") ? open : !isOpen;
zoomPick.setAttribute("data-open", next ? "1" : "0");
try {
if (next) {
positionZoomMenu();
zoomMenu.style.display = "block";
} else {
zoomMenu.style.display = "none";
}
} catch {}
};
zoomBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
toggleZoomMenu();
});
const onZoomDocDown = (e) => {
try {
if (!zoomPick.contains(e.target) && !zoomMenu.contains(e.target)) {
zoomPick.setAttribute("data-open", "0");
zoomMenu.style.display = "none";
}
} catch {}
};
document.addEventListener("pointerdown", onZoomDocDown, true);
try { pop._wmeRcZoomDown = onZoomDocDown; } catch {}
try { window.addEventListener("resize", positionZoomMenu, true); } catch {}
try { window.addEventListener("scroll", positionZoomMenu, true); } catch {}
const cleanupZoomPick = () => {
try { document.removeEventListener("pointerdown", onZoomDocDown, true); } catch {}
try { window.removeEventListener("resize", positionZoomMenu, true); } catch {}
try { window.removeEventListener("scroll", positionZoomMenu, true); } catch {}
try { zoomMenu.remove(); } catch {}
};
try { pop._wmeRcCleanupZoomPick = cleanupZoomPick; } catch {}
try {
const mo = new MutationObserver(() => {
try {
if (!document.body.contains(modal)) {
cleanupZoomPick();
mo.disconnect();
}
} catch {}
});
mo.observe(document.body, { childList: true, subtree: true });
} catch {}
namesChk.addEventListener("change", () => {
try { setPinsShowNamesOnMap(!!namesChk.checked); } catch {}
updateZoomDisabled();
try { refreshPinsMarkers(true); } catch {}
});
body.appendChild(namesWrap);
const emptyWrap = document.createElement("div");
emptyWrap.className = "wmeRcHint";
emptyWrap.innerHTML = `
<label class="wmeRcInlineToggle">
<input type="checkbox" class="wmeRcAlwaysVisChk">
<span class="wmeRcSwitchTrack"><span class="wmeRcSwitchThumb"></span></span>
<span class="wmeRcSwitchLabelWrap"><span class="wmeRcSwitchLabel">Show panel when no pins</span><span class="wmeRcInfoQ" tabindex="0">?<span class="wmeRcInfoTip">When "disabled", the panel is not shown unless you have a place pinned.</span></span></span>
</label>
`;
const emptyChk = emptyWrap.querySelector("input");
emptyChk.checked = !!getPinsPanelAlwaysVisibleEmpty();
const defTitle = document.createElement("div");
defTitle.className = "wmeRcHint";
defTitle.textContent = "Default folder name";
const defRow = document.createElement("div");
defRow.className = "wmeRcRow";
try { defRow.style.display = "flex"; defRow.style.gap = "10px"; defRow.style.alignItems = "center"; } catch {}
const defInp = document.createElement("input");
defInp.className = "wmeRcInput";
defInp.type = "text";
defInp.placeholder = "(no folder)";
defInp.maxLength = 32;
defInp.value = String(getGroupName("default") || "(no folder)");
defInp.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") { try { ev.preventDefault(); } catch {} }
});
defRow.appendChild(defInp);
const soundTitle = document.createElement("div");
soundTitle.className = "wmeRcHint";
soundTitle.textContent = "Reminder sound";
const posTitle = document.createElement("div");
posTitle.className = "wmeRcHint";
posTitle.textContent = "Notification Position";
const posRow = document.createElement("div");
posRow.className = "wmeRcRow";
try { posRow.style.display = "flex"; posRow.style.gap = "10px"; posRow.style.alignItems = "center"; } catch {}
const posPick = document.createElement("div");
posPick.className = "wmeRcSoundPick";
posPick.tabIndex = 0;
const posBtn = document.createElement("div");
posBtn.className = "wmeRcSoundBtn";
const posBtnLabel = document.createElement("div");
posBtnLabel.className = "wmeRcSoundBtnLabel";
const posCaret = document.createElement("div");
posCaret.className = "wmeRcSoundCaret";
posCaret.innerHTML = ICONS.chevDown;
posBtn.appendChild(posBtnLabel);
posBtn.appendChild(posCaret);
posPick.appendChild(posBtn);
const posMenu = document.createElement("div");
posMenu.className = "wmeRcSoundMenu";
posPick.appendChild(posMenu);
let selectedPosId = getReminderNoticePosition();
const getPosLabelFor = (id) => {
const o = REMINDER_NOTICE_POSITION_OPTIONS.find(x => x.id === id);
return o ? o.label : "Right";
};
const setSelectedPos = (id) => {
selectedPosId = REMINDER_NOTICE_POSITION_OPTIONS.some(o => o.id === id) ? id : "right";
posBtnLabel.textContent = getPosLabelFor(selectedPosId);
try { setReminderNoticePosition(selectedPosId); } catch {}
try { posPick.setAttribute("data-open", "0"); } catch {}
try { if (posMenu && posMenu.parentNode === document.body) { posMenu.style.display = "none"; posMenu.remove(); } } catch {}
};
for (const opt of REMINDER_NOTICE_POSITION_OPTIONS) {
const item = document.createElement("div");
item.className = "wmeRcSoundItem";
item.textContent = opt.label;
item.dataset.posId = opt.id;
item.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
setSelectedPos(opt.id);
});
posMenu.appendChild(item);
}
setSelectedPos(selectedPosId);
const closePosMenu = () => {
try { posPick.setAttribute("data-open", "0"); } catch {}
try {
if (posMenu && posMenu.parentNode === document.body) { posMenu.style.display = "none"; posMenu.remove(); }
else if (posMenu) { posMenu.style.display = "none"; }
} catch {}
posMenuOpen = false;
};
let posMenuOpen = false;
const openPosMenu = () => {
try {
if (!posMenu) return;
if (posMenu.parentNode !== document.body) {
try { posMenu.remove(); } catch {}
document.body.appendChild(posMenu);
}
const r = posBtn.getBoundingClientRect();
const top = Math.round(r.bottom + 6);
const left = Math.round(r.left);
const width = Math.round(r.width);
posMenu.style.position = "fixed";
posMenu.style.left = left + "px";
posMenu.style.top = top + "px";
posMenu.style.width = width + "px";
posMenu.style.right = "auto";
posMenu.style.zIndex = "2147483647";
posMenu.style.display = "block";
const maxH = Math.max(120, window.innerHeight - top - 12);
posMenu.style.maxHeight = Math.min(240, maxH) + "px";
try { posMenu.style.overflow = "hidden"; } catch {}
try { posPick.setAttribute("data-open", "1"); } catch {}
posMenuOpen = true;
} catch {}
};
const togglePosMenu = (open) => {
const next = (typeof open === "boolean") ? open : !posMenuOpen;
if (next) openPosMenu();
else closePosMenu();
};
posBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
togglePosMenu();
});
const onPosDocDown = (e) => {
try {
if (!posMenuOpen) return;
if (posPick.contains(e.target)) return;
if (posMenu && posMenu.contains(e.target)) return;
closePosMenu();
} catch {}
};
document.addEventListener("pointerdown", onPosDocDown, true);
try {
const reposition = () => { if (posMenuOpen) openPosMenu(); };
window.addEventListener("resize", reposition, { passive: true });
window.addEventListener("scroll", reposition, true);
} catch {}
try {
const mo = new MutationObserver(() => {
try {
if (!document.body.contains(modal)) {
try { document.removeEventListener("pointerdown", onPosDocDown, true); } catch {}
try { closePosMenu(); } catch {}
mo.disconnect();
}
} catch {}
});
mo.observe(document.body, { childList: true });
} catch {}
posRow.appendChild(posPick);
const soundRow = document.createElement("div");
soundRow.className = "wmeRcRow";
try { soundRow.style.display = "flex"; soundRow.style.gap = "10px"; soundRow.style.alignItems = "center"; } catch {}
const soundPick = document.createElement("div");
soundPick.className = "wmeRcSoundPick";
soundPick.tabIndex = 0;
const soundBtn = document.createElement("div");
soundBtn.className = "wmeRcSoundBtn";
const soundBtnLabel = document.createElement("div");
soundBtnLabel.className = "wmeRcSoundBtnLabel";
const soundCaret = document.createElement("div");
soundCaret.className = "wmeRcSoundCaret";
soundCaret.innerHTML = ICONS.chevDown;
soundBtn.appendChild(soundBtnLabel);
soundBtn.appendChild(soundCaret);
soundPick.appendChild(soundBtn);
const soundMenu = document.createElement("div");
soundMenu.className = "wmeRcSoundMenu wmeRcSoundMenuPortal";
soundMenu.style.display = "none";
try { document.body.appendChild(soundMenu); } catch {}
const positionSoundMenu = () => {
try {
const r = soundBtn.getBoundingClientRect();
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
const w = Math.max(220, Math.min(r.width || 260, vw - 16));
let left = Math.max(8, Math.min(r.left, vw - w - 8));
let top = (r.bottom || 0) + 6;
const maxH = 240;
if (top + maxH + 8 > vh) {
top = Math.max(8, (r.top || 0) - 6 - maxH);
}
soundMenu.style.left = left + "px";
soundMenu.style.top = top + "px";
soundMenu.style.width = w + "px";
const avail = Math.max(120, vh - top - 10);
soundMenu.style.maxHeight = Math.min(maxH, avail) + "px";
} catch {}
};
let selectedSoundId = getReminderSoundId();
const getLabelFor = (id) => {
const o = REMINDER_SOUND_OPTIONS.find(x => x.id === id);
return o ? o.label : "Classic bell";
};
const setSelectedSound = (id, play = false) => {
selectedSoundId = REMINDER_SOUND_OPTIONS.some(o => o.id === id) ? id : "bell";
soundBtnLabel.textContent = getLabelFor(selectedSoundId);
try { soundPick.setAttribute("data-open", "0"); } catch {}
try { soundMenu.style.display = "none"; } catch {}
if (play && selectedSoundId !== "mute") { try { playReminderSoundOnce(selectedSoundId, { force: true }); } catch {} }
};
for (const opt of REMINDER_SOUND_OPTIONS) {
const item = document.createElement("div");
item.className = "wmeRcSoundItem";
item.textContent = opt.label;
item.dataset.soundId = opt.id;
item.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
setSelectedSound(opt.id, true);
});
soundMenu.appendChild(item);
}
setSelectedSound(selectedSoundId, false);
const toggleMenu = (open) => {
const isOpen = soundPick.getAttribute("data-open") === "1";
const next = (typeof open === "boolean") ? open : !isOpen;
soundPick.setAttribute("data-open", next ? "1" : "0");
try {
if (next) {
positionSoundMenu();
soundMenu.style.display = "block";
} else {
soundMenu.style.display = "none";
}
} catch {}
};
soundBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
toggleMenu();
});
const onDocDown = (e) => {
try {
if (!soundPick.contains(e.target) && !soundMenu.contains(e.target)) { soundPick.setAttribute("data-open", "0"); try { soundMenu.style.display = "none"; } catch {} }
} catch {}
};
document.addEventListener("pointerdown", onDocDown, true);
try { pop._wmeRcDown = onDocDown; } catch {}
try { window.addEventListener("resize", positionSoundMenu, true); } catch {}
try { window.addEventListener("scroll", positionSoundMenu, true); } catch {}
const cleanupSoundPick = () => {
try { document.removeEventListener("pointerdown", onDocDown, true); } catch {}
try { window.removeEventListener("resize", positionSoundMenu, true); } catch {}
try { window.removeEventListener("scroll", positionSoundMenu, true); } catch {}
try { soundMenu.remove(); } catch {}
};
const soundPrev = document.createElement("div");
soundPrev.className = "wmeRcMiniBtn";
soundPrev.textContent = "▶";
soundPrev.title = "Preview";
soundPrev.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
if (selectedSoundId === "mute") return;
try { playReminderSoundOnce(selectedSoundId, { force: true }); } catch {}
});
soundRow.appendChild(soundPick);
soundRow.appendChild(soundPrev);
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const btnCancel = document.createElement("div");
btnCancel.className = "wmeRcModalBtn";
btnCancel.textContent = "Close";
btnCancel.addEventListener("click", () => { try { if (typeof cleanupSoundPick === "function") cleanupSoundPick(); } catch {} close(); });
const btnSave = document.createElement("div");
btnSave.className = "wmeRcModalBtn primary";
btnSave.textContent = "Save";
btnSave.addEventListener("click", () => {
setReminderSoundId(String(selectedSoundId || "bell"));
try { setPinsPanelAlwaysVisibleEmpty(!!(emptyChk && emptyChk.checked)); } catch {}
try { setPinsMinimizeMode(String(pendingMinMode || "bubble")); } catch {}
try { setDefaultGroupName(defInp ? defInp.value : "(no folder)"); } catch {}
toast("Settings saved");
try { renderPinsPanel(); } catch {}
try { if (typeof cleanupSoundPick === "function") cleanupSoundPick(); } catch {}
close();
});
actions.appendChild(btnCancel);
actions.appendChild(btnSave);
body.appendChild(visWrap);
body.appendChild(namesWrap);
body.appendChild(emptyWrap);
// Minimize behavior
const minModeTitle = document.createElement("div");
minModeTitle.className = "wmeRcHint";
minModeTitle.textContent = "Minimize as";
let pendingMinMode = "bubble";
try { pendingMinMode = getPinsMinimizeMode(); } catch { pendingMinMode = "bubble"; }
const minModeRow = document.createElement("div");
minModeRow.className = "wmeRcRow";
try { minModeRow.style.display = "flex"; minModeRow.style.gap = "10px"; minModeRow.style.alignItems = "center"; } catch {}
const minModePick = document.createElement("div");
minModePick.className = "wmeRcSoundPick";
minModePick.tabIndex = 0;
const minModeBtn = document.createElement("div");
minModeBtn.className = "wmeRcSoundBtn";
const minModeBtnLabel = document.createElement("div");
minModeBtnLabel.className = "wmeRcSoundBtnLabel";
const minModeCaret = document.createElement("div");
minModeCaret.className = "wmeRcSoundCaret";
try { minModeCaret.innerHTML = ICONS.chevDown; } catch { try { minModeCaret.textContent = "▾"; } catch {} }
minModeBtn.appendChild(minModeBtnLabel);
minModeBtn.appendChild(minModeCaret);
minModePick.appendChild(minModeBtn);
const minModeMenu = document.createElement("div");
minModeMenu.className = "wmeRcSoundMenu wmeRcSoundMenuPortal";
minModeMenu.style.display = "none";
try { document.body.appendChild(minModeMenu); } catch {}
const setMinModeLabel = () => {
try { minModeBtnLabel.textContent = (pendingMinMode === "panel") ? "Panel" : "Bubble"; } catch {}
};
setMinModeLabel();
const closeMinModeMenu = () => {
try { minModePick.setAttribute("data-open", "0"); } catch {}
try { minModeMenu.style.display = "none"; } catch {}
minModeMenuOpen = false;
};
let minModeMenuOpen = false;
const openMinModeMenu = () => {
try {
const r = minModeBtn.getBoundingClientRect();
const top = Math.round(r.bottom + 6);
const left = Math.round(r.left);
const width = Math.round(r.width);
minModeMenu.style.position = "fixed";
minModeMenu.style.left = left + "px";
minModeMenu.style.top = top + "px";
minModeMenu.style.width = width + "px";
minModeMenu.style.right = "auto";
minModeMenu.style.zIndex = "2147483647";
minModeMenu.style.display = "block";
const maxH = Math.max(120, window.innerHeight - top - 12);
minModeMenu.style.maxHeight = Math.min(240, maxH) + "px";
try { minModePick.setAttribute("data-open", "1"); } catch {}
minModeMenuOpen = true;
} catch {}
};
const toggleMinModeMenu = (open) => {
const next = (typeof open === "boolean") ? open : !minModeMenuOpen;
if (next) openMinModeMenu();
else closeMinModeMenu();
};
const buildMinModeMenu = () => {
minModeMenu.innerHTML = "";
const makeItem = (value, label) => {
const it = document.createElement("div");
it.className = "wmeRcSoundItem";
it.textContent = label;
it.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
pendingMinMode = (value === "panel") ? "panel" : "bubble";
setMinModeLabel();
buildMinModeMenu();
closeMinModeMenu();
});
return it;
};
minModeMenu.appendChild(makeItem("bubble", "Bubble"));
minModeMenu.appendChild(makeItem("panel", "Panel"));
};
buildMinModeMenu();
minModeBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
toggleMinModeMenu();
});
const onDocDownMinMode = (e) => {
try {
if (!minModeMenuOpen) return;
if (minModePick.contains(e.target)) return;
if (minModeMenu && minModeMenu.contains(e.target)) return;
closeMinModeMenu();
} catch {}
};
document.addEventListener("pointerdown", onDocDownMinMode, true);
const repositionMinModeMenu = () => { if (minModeMenuOpen) openMinModeMenu(); };
try { window.addEventListener("resize", repositionMinModeMenu, { passive: true }); } catch {}
try { window.addEventListener("scroll", repositionMinModeMenu, true); } catch {}
body.appendChild(minModeTitle);
minModeRow.appendChild(minModePick);
body.appendChild(minModeRow);
body.appendChild(defTitle);
body.appendChild(defRow);
body.appendChild(soundTitle);
body.appendChild(soundRow);
body.appendChild(posTitle);
body.appendChild(posRow);
body.appendChild(actions);
},
});
}
function openPinsManagerLightbox() {
try { ensureCSS(); } catch {}
openModal({
title: "Pins manager",
iconSvg: ICONS.properties || ICONS.tools,
bodyBuilder: ({ body, close, modal }) => {
try { modal.classList.add("wmeRcLightbox"); } catch {}
const groups = () => loadPinGroups();
const pins = () => loadPins();
let selGid = normalizeGroupId(getSelectedPinsGroupId() || "default");
let qFolders = "";
let qPins = "";
const root = document.createElement("div");
root.style.width = "100%";
root.style.height = "100%";
root.style.display = "flex";
root.style.flexDirection = "row";
root.style.gap = "0";
root.style.minHeight = "0";
const left = document.createElement("div");
left.className = "wmeRcPmLeft";
const right = document.createElement("div");
right.className = "wmeRcPmRight";
// ----- Left: folders -----
const leftHdr = document.createElement("div");
leftHdr.className = "wmeRcPmHdrRow";
const leftTitle = document.createElement("div");
leftTitle.style.fontWeight = "900";
leftTitle.style.letterSpacing = ".2px";
leftTitle.textContent = "Folders";
const addFolderBtn = document.createElement("div");
addFolderBtn.className = "wmeRcPmTiny";
addFolderBtn.title = "Add folder";
addFolderBtn.innerHTML = `<span class="wmeRcI">${ICONS.folderPlus}</span>`;
addFolderBtn.addEventListener("click", (ev) => {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
openGroupNameModal({
title: "New folder",
placeholder: "Name",
okText: "Create",
onSubmit: (name, emoji) => {
const gs = groups();
const id = `g-${_uid()}`;
gs.push({ id, name, emoji: (typeof emoji === "string") ? emoji : "" });
savePinGroups(gs);
selGid = id;
try { setSelectedPinsGroupId(id); } catch {}
render();
}
});
});
leftHdr.appendChild(leftTitle);
leftHdr.appendChild(addFolderBtn);
const folderSearch = document.createElement("input");
folderSearch.className = "wmeRcInput wmeRcPmSearch";
folderSearch.placeholder = "Search folders…";
folderSearch.addEventListener("input", () => { qFolders = String(folderSearch.value || "").trim().toLowerCase(); renderFolders(); });
const folderList = document.createElement("div");
folderList.className = "wmeRcPmList";
left.appendChild(leftHdr);
left.appendChild(folderSearch);
left.appendChild(folderList);
// ----- Right: pins -----
const rightHdr = document.createElement("div");
rightHdr.className = "wmeRcPmHdrRow";
const rightTitle = document.createElement("div");
rightTitle.style.fontWeight = "900";
rightTitle.style.letterSpacing = ".2px";
rightTitle.textContent = "Pins";
const pinSearch = document.createElement("input");
pinSearch.className = "wmeRcInput wmeRcPmSearch";
pinSearch.placeholder = "Search pins…";
pinSearch.addEventListener("input", () => { qPins = String(pinSearch.value || "").trim().toLowerCase(); renderPins(); });
const pinsList = document.createElement("div");
pinsList.className = "wmeRcPmList";
right.appendChild(rightHdr);
right.appendChild(pinSearch);
right.appendChild(pinsList);
root.appendChild(left);
root.appendChild(right);
body.appendChild(root);
function getGroupMeta(gid) {
gid = normalizeGroupId(gid || "default");
if (gid === "default") return { id: "default", name: "General", emoji: "" };
const g = groups().find(x => x && x.id === gid);
return g || { id: gid, name: "Folder", emoji: "" };
}
function countPinsInGroup(gid) {
gid = normalizeGroupId(gid || "default");
let c = 0;
for (const p of pins()) if (normalizeGroupId(p.groupId || "default") === gid) c++;
return c;
}
function mkTiny(icon, title, onClick) {
const b = document.createElement("div");
b.className = "wmeRcPmTiny";
b.title = title;
b.innerHTML = `<span class="wmeRcI">${icon}</span>`;
b.addEventListener("click", (ev) => { try { ev.preventDefault(); ev.stopPropagation(); } catch {} onClick && onClick(); });
return b;
}
function renderFolders() {
folderList.innerHTML = "";
const gs = groups();
// Default folder first
const allFolderIds = ["default", ...gs.map(g => g.id)];
for (const gid0 of allFolderIds) {
const meta = getGroupMeta(gid0);
const nm = (meta.emoji ? (meta.emoji + " ") : "") + String(meta.name || "");
const c = countPinsInGroup(gid0);
if (qFolders) {
const hay = (String(meta.name || "") + " " + String(meta.emoji || "")).toLowerCase();
if (!hay.includes(qFolders)) continue;
}
const card = document.createElement("div");
card.className = "wmeRcPmCard" + (normalizeGroupId(gid0) === normalizeGroupId(selGid) ? " on" : "");
card.addEventListener("click", () => {
selGid = normalizeGroupId(gid0);
try { setSelectedPinsGroupId(selGid); } catch {}
render();
});
const l = document.createElement("div");
l.className = "wmeRcPmCardLeft";
const t = document.createElement("div");
t.className = "wmeRcPmCardTitle";
t.textContent = nm || "Folder";
const s = document.createElement("div");
s.className = "wmeRcPmCardSub";
s.textContent = `${c} pin${c === 1 ? "" : "s"}`;
l.appendChild(t);
l.appendChild(s);
const btns = document.createElement("div");
btns.className = "wmeRcPmCardBtns";
// Rename (not for default)
if (gid0 !== "default") {
btns.appendChild(mkTiny(ICONS.edit, "Rename folder", () => {
const g = gs.find(x => x && x.id === gid0);
openGroupNameModal({
title: "Rename folder",
placeholder: "Name",
okText: "Save",
initial: g ? g.name : "",
initialEmoji: g ? (g.emoji || "") : "",
onSubmit: (name, emoji) => {
const arr = groups();
const gg = arr.find(x => x && x.id === gid0);
if (gg) { gg.name = name; gg.emoji = (typeof emoji === "string") ? emoji : ""; }
savePinGroups(arr);
render();
}
});
}));
btns.appendChild(mkTiny(ICONS.trash, "Remove folder", () => {
openRemoveFolderModal(gid0);
// openRemoveFolderModal triggers re-render via existing flows; also refresh after a tick
setTimeout(() => { try { render(); } catch {} }, 250);
}));
} else {
btns.appendChild(mkTiny(ICONS.trash, "Clear pins in General", () => {
const c = countPinsInGroup("default");
openClearDefaultFolderPinsModal({
folderName: "General",
count: c,
onConfirm: () => {
const all = pins().filter(p => normalizeGroupId(p.groupId || "default") !== "default");
savePins(all);
render();
try { renderPinsPanel(); } catch {}
try { renderPinsMarkers(); } catch {}
}
});
}));
}
card.appendChild(l);
card.appendChild(btns);
folderList.appendChild(card);
}
}
function renderPins() {
pinsList.innerHTML = "";
const gid = normalizeGroupId(selGid || "default");
const meta = getGroupMeta(gid);
// Header: folder name + quick actions
rightHdr.innerHTML = "";
const title = document.createElement("div");
title.style.fontWeight = "900";
title.style.letterSpacing = ".2px";
title.textContent = `Pins — ${meta.emoji ? (meta.emoji + " ") : ""}${meta.name || "General"}`;
const hdrBtns = document.createElement("div");
hdrBtns.style.display = "flex";
hdrBtns.style.gap = "8px";
hdrBtns.style.alignItems = "center";
const closeBtn = mkTiny(ICONS.chevDown, "Close", () => close());
// make it an X-like feel by rotating chevron
try { closeBtn.querySelector("svg").style.transform = "rotate(90deg)"; } catch {}
hdrBtns.appendChild(closeBtn);
rightHdr.appendChild(title);
rightHdr.appendChild(hdrBtns);
const gs = groups();
const folderOptions = [{ id: "default", name: "General", emoji: "" }, ...gs.map(g => ({ id: g.id, name: g.name, emoji: g.emoji || "" }))];
const list = pins().filter(p => normalizeGroupId(p.groupId || "default") === gid);
const q = qPins;
const filtered = q ? list.filter(p => String(p.name || "").toLowerCase().includes(q)) : list;
if (!filtered.length) {
const empty = document.createElement("div");
empty.className = "wmeRcHint";
empty.style.opacity = ".8";
empty.textContent = q ? "No pins match your search." : "No pins in this folder.";
pinsList.appendChild(empty);
return;
}
for (const p of filtered) {
const row = document.createElement("div");
row.className = "wmeRcPmRow";
const l = document.createElement("div");
l.className = "wmeRcPmRowLeft";
const t = document.createElement("div");
t.className = "wmeRcPmRowTitle";
t.textContent = String(p.name || "Pin");
const sub = document.createElement("div");
sub.className = "wmeRcPmRowSub";
const lat = (Number(p.lat) || 0).toFixed(6);
const lon = (Number(p.lon) || 0).toFixed(6);
sub.textContent = `${lat}, ${lon}`;
l.appendChild(t);
l.appendChild(sub);
const btns = document.createElement("div");
btns.className = "wmeRcPmRowBtns";
// Move to folder (select)
const sel = document.createElement("select");
sel.className = "wmeRcSelect wmeRcPmSelect";
for (const fo of folderOptions) {
const o = document.createElement("option");
o.value = fo.id;
o.textContent = (fo.emoji ? (fo.emoji + " ") : "") + String(fo.name || (fo.id === "default" ? "General" : "Folder"));
if (normalizeGroupId(p.groupId || "default") === normalizeGroupId(fo.id)) o.selected = true;
sel.appendChild(o);
}
sel.addEventListener("change", () => {
const to = normalizeGroupId(sel.value || "default");
updatePin(p.id, { groupId: to });
// stay on current folder view; just refresh
render();
try { renderPinsPanel(); } catch {}
try { renderPinsMarkers(); } catch {}
});
btns.appendChild(sel);
btns.appendChild(mkTiny(ICONS.edit, "Rename pin", () => {
openRenamePinModal(p.id);
setTimeout(() => { try { render(); } catch {} }, 250);
}));
btns.appendChild(mkTiny(ICONS.trash, "Remove pin", () => {
confirmRemovePin(p.id);
setTimeout(() => { try { render(); } catch {} }, 250);
}));
row.appendChild(l);
row.appendChild(btns);
pinsList.appendChild(row);
}
}
function render() {
// if selected folder removed, fall back
const gid = normalizeGroupId(selGid || "default");
const exists = gid === "default" || groups().some(g => g && g.id === gid);
if (!exists) selGid = "default";
renderFolders();
renderPins();
}
render();
}
});
}
function openReminderModal(pinId) {
const pin = loadPins().find((p) => p.id === String(pinId));
if (!pin) return;
const now = Date.now();
const currentTs = pin.reminderAt && pin.reminderAt > now ? pin.reminderAt : null;
let mode = "IN"; // always start on In tab
let unit = (pin.reminderUnit === "HOURS") ? "HOURS" : "MINUTES";
let inValue = Number.isFinite(pin.reminderValue) ? Math.max(1, Math.round(pin.reminderValue)) : (unit === "HOURS" ? 1 : 30);
let atDate = "";
let atTime = "";
if (!currentTs) {
try {
const d0 = new Date(now);
mode = "AT";
atDate = `${String(d0.getFullYear())}-${String(d0.getMonth() + 1).padStart(2, "0")}-${String(d0.getDate()).padStart(2, "0")}`;
atTime = `${String(d0.getHours()).padStart(2, "0")}:${String(d0.getMinutes()).padStart(2, "0")}`;
} catch {}
}
if (currentTs) {
const d = new Date(currentTs);
const yyyy = String(d.getFullYear());
const mm = String(d.getMonth() + 1).padStart(2, "0");
const dd = String(d.getDate()).padStart(2, "0");
const hh = String(d.getHours()).padStart(2, "0");
const mi = String(d.getMinutes()).padStart(2, "0");
atDate = `${yyyy}-${mm}-${dd}`;
atTime = `${hh}:${mi}`;
}
const prettyCurrent = () => {
try {
if (!currentTs) return "No reminder set yet.";
const d = new Date(currentTs);
return `Current reminder: ${d.toLocaleString()}`;
} catch {
return "Reminder set.";
}
};
const getMaxForUnit = () => (unit === "HOURS" ? 48 : 180);
const getStepForUnit = () => (unit === "HOURS" ? 1 : 1);
const calcInTargetTs = () => {
const val = Math.max(1, Math.round(inValue));
const mins = unit === "HOURS" ? val * 60 : val;
return Date.now() + mins * 60000;
};
const calcAtTargetTs = () => {
try {
if (!atDate || !atTime) return null;
if (!/^\d{2}:\d{2}$/.test(atTime)) return null;
const dt = new Date(`${atDate}T${atTime}:00`);
const t = dt.getTime();
return Number.isFinite(t) ? t : null;
} catch {
return null;
}
};
const fmtPreview = (ts) => {
try {
const d = new Date(ts);
const dd = String(d.getDate()).padStart(2, "0");
const mo = String(d.getMonth() + 1).padStart(2, "0");
const yy = String(d.getFullYear());
const hh = String(d.getHours()).padStart(2, "0");
const mi = String(d.getMinutes()).padStart(2, "0");
return `${dd}/${mo}/${yy}, ${hh}:${mi}`;
} catch {
return "";
}
};
const makeClock = ({ getValue, setValue, getMax, getStep, labelFn, getTargetTs, getUnitLabel }) => {
const wrap = document.createElement("div");
wrap.className = "wmeRcClockWrap";
const clock = document.createElement("div");
clock.className = "wmeRcClock";
const ticks = document.createElement("div");
ticks.className = "wmeRcClockTicks";
for (let i = 0; i < 12; i++) {
const t = document.createElement("div");
t.className = "wmeRcClockTick";
t.style.transform = `translate(-50%,-68px) rotate(${i * 30}deg) translate(0, 68px)`;
ticks.appendChild(t);
}
const nums = document.createElement("div");
nums.className = "wmeRcClockNums";
for (let n = 1; n <= 12; n++) {
const sp = document.createElement("div");
sp.className = "wmeRcClockNum";
sp.textContent = String(n === 12 ? 12 : n);
const ang = ((n % 12) / 12) * Math.PI * 2 - (Math.PI / 2);
const r = 52;
const x = Math.cos(ang) * r;
const y = Math.sin(ang) * r;
sp.style.transform = `translate(-50%,-50%) translate(${x.toFixed(1)}px, ${y.toFixed(1)}px)`;
nums.appendChild(sp);
}
const handMin = document.createElement("div");
handMin.className = "wmeRcClockHand min";
const handHour = document.createElement("div");
handHour.className = "wmeRcClockHand hour";
const handSec = document.createElement("div");
handSec.className = "wmeRcClockHand sec";
const center = document.createElement("div");
center.className = "wmeRcClockCenter";
clock.appendChild(ticks);
clock.appendChild(nums);
clock.appendChild(handHour);
clock.appendChild(handSec);
clock.appendChild(handMin);
clock.appendChild(center);
try {
clock.style.touchAction = "none";
handMin.style.cursor = "grab";
let dragging = false;
let dragPid = null;
const timeToParts = (t) => {
const m = String(t || "").match(/^(\d{2}):(\d{2})$/);
if (!m) return { hh: 0, mm: 0 };
return { hh: Math.max(0, Math.min(23, parseInt(m[1], 10))), mm: Math.max(0, Math.min(59, parseInt(m[2], 10))) };
};
const setMinuteFromClient = (clientX, clientY) => {
const r = clock.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = clientX - cx;
const dy = clientY - cy;
let ang = Math.atan2(dy, dx); // 0 at +x, CCW
let deg = (ang * 180 / Math.PI) + 90;
deg = (deg + 360) % 360;
const minute = Math.round(deg / 6) % 60;
const cur = timeToParts(getTime());
const hh = cur.hh;
const mm = minute;
const next = `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
if (typeof setTime === "function") setTime(next);
update();
};
const onMove = (ev) => {
if (!dragging) return;
if (dragPid != null && ev.pointerId != null && ev.pointerId !== dragPid) return;
try { ev.preventDefault(); } catch {}
setMinuteFromClient(ev.clientX, ev.clientY);
};
const endDrag = (ev) => {
if (dragPid != null && ev.pointerId != null && ev.pointerId !== dragPid) return;
dragging = false;
dragPid = null;
try { handMin.releasePointerCapture(ev.pointerId); } catch {}
try { window.removeEventListener("pointermove", onMove, true); } catch {}
try { window.removeEventListener("pointerup", endDrag, true); } catch {}
try { window.removeEventListener("pointercancel", endDrag, true); } catch {}
try { handMin.style.cursor = "grab"; } catch {}
};
handMin.addEventListener("pointerdown", (ev) => {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
dragging = true;
dragPid = ev.pointerId;
try { handMin.setPointerCapture(ev.pointerId); } catch {}
try { handMin.style.cursor = "grabbing"; } catch {}
setMinuteFromClient(ev.clientX, ev.clientY);
window.addEventListener("pointermove", onMove, true);
window.addEventListener("pointerup", endDrag, true);
window.addEventListener("pointercancel", endDrag, true);
}, true);
} catch {}
const valueBox = document.createElement("div");
valueBox.className = "wmeRcClockValue";
const big = document.createElement("div");
big.className = "wmeRcBigValue wmeRcEditableValue";
big.title = "Click to edit";
const sub = document.createElement("div");
sub.className = "wmeRcBigValueSub";
const editWrap = document.createElement("div");
editWrap.style.display = "none";
editWrap.style.alignItems = "center";
editWrap.style.gap = "8px";
const editInp = document.createElement("input");
editInp.type = "text";
editInp.inputMode = "numeric";
editInp.autocomplete = "off";
editInp.spellcheck = false;
editInp.className = "wmeRcInput";
try {
editInp.style.width = "86px";
editInp.style.padding = "8px 10px";
editInp.style.borderRadius = "12px";
editInp.style.fontWeight = "900";
editInp.style.fontSize = "22px";
editInp.style.textAlign = "center";
editInp.style.color = "#fff";
editInp.style.background = "rgba(255,255,255,.06)";
editInp.style.border = "1px solid rgba(255,255,255,.18)";
editInp.style.boxShadow = "inset 0 1px 0 rgba(255,255,255,.06)";
editInp.style.outline = "none";
} catch {}
const editUnit = document.createElement("div");
editUnit.style.fontWeight = "900";
editUnit.style.opacity = ".92";
editUnit.style.fontSize = "14px";
editUnit.style.whiteSpace = "nowrap";
editWrap.appendChild(editInp);
editWrap.appendChild(editUnit);
const beginEdit = () => {
try {
editUnit.textContent = (typeof getUnitLabel === "function") ? getUnitLabel() : "";
} catch { editUnit.textContent = ""; }
const cur = Math.max(1, Math.round(getValue()));
editInp.value = String(cur);
editWrap.style.display = "flex";
big.style.display = "none";
try { setTimeout(() => { try { editInp.focus(); editInp.select(); } catch {} }, 0); } catch {}
};
const endEdit = (commit) => {
if (commit) {
const v = parseInt(String(editInp.value || "").replace(/[^0-9]/g, ""), 10);
if (Number.isFinite(v)) {
setValue(clamp(v, 1, getMax()));
}
}
editWrap.style.display = "none";
big.style.display = "";
update();
};
try {
big.style.cursor = "text";
big.tabIndex = 0;
big.addEventListener("click", () => beginEdit());
big.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") { try { ev.preventDefault(); } catch {} beginEdit(); }
});
editInp.addEventListener("input", () => {
try { editInp.value = String(editInp.value || "").replace(/[^0-9]/g, ""); } catch {}
});
editInp.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") { try { ev.preventDefault(); } catch {} endEdit(true); }
else if (ev.key === "Escape") { try { ev.preventDefault(); } catch {} endEdit(false); }
});
editInp.addEventListener("blur", () => endEdit(true));
} catch {}
const stepper = document.createElement("div");
stepper.className = "wmeRcStepper";
const btnMinus = document.createElement("div");
btnMinus.className = "wmeRcStepBtn";
btnMinus.textContent = "−";
const btnPlus = document.createElement("div");
btnPlus.className = "wmeRcStepBtn";
btnPlus.textContent = "+";
stepper.appendChild(btnMinus);
stepper.appendChild(btnPlus);
valueBox.appendChild(big);
valueBox.appendChild(editWrap);
valueBox.appendChild(sub);
valueBox.appendChild(stepper);
wrap.appendChild(clock);
wrap.appendChild(valueBox);
let _lastMinDeg = null, _minOffsetDeg = 0;
let _lastHourDeg = null, _hourOffsetDeg = 0;
const _applySmoothDeg = (rawDeg, lastDeg, offsetDeg) => {
let deg = rawDeg + offsetDeg;
if (lastDeg != null) {
if (deg < lastDeg - 180) { offsetDeg += 360; deg = rawDeg + offsetDeg; }
else if (deg > lastDeg + 180) { offsetDeg -= 360; deg = rawDeg + offsetDeg; }
}
return { deg, offsetDeg };
};
const setHandsFromTs = (ts) => {
try {
const d = new Date(ts);
const hh = d.getHours();
const mm = d.getMinutes();
const ss = d.getSeconds();
const h = (hh % 12) + (mm / 60);
const rawH = (h / 12) * 360;
const rawM = (mm / 60) * 360;
const rawS = (ss / 60) * 360;
const hhRes = _applySmoothDeg(rawH, _lastHourDeg, _hourOffsetDeg);
_hourOffsetDeg = hhRes.offsetDeg;
_lastHourDeg = hhRes.deg;
const mmRes = _applySmoothDeg(rawM, _lastMinDeg, _minOffsetDeg);
_minOffsetDeg = mmRes.offsetDeg;
_lastMinDeg = mmRes.deg;
handHour.style.transform = `translate(-50%,-100%) rotate(${hhRes.deg}deg)`;
handMin.style.transform = `translate(-50%,-100%) rotate(${mmRes.deg}deg)`;
try { handSec.style.transform = `translate(-50%,-100%) rotate(${rawS}deg)`; } catch {}
} catch {
_lastMinDeg = null; _minOffsetDeg = 0;
_lastHourDeg = null; _hourOffsetDeg = 0;
handHour.style.transform = `translate(-50%,-100%) rotate(0deg)`;
handMin.style.transform = `translate(-50%,-100%) rotate(0deg)`;
try { handSec.style.transform = `translate(-50%,-100%) rotate(0deg)`; } catch {}
}
};
const fmtTime = (ts) => {
try {
const d = new Date(ts);
const hh = String(d.getHours()).padStart(2, "0");
const mm = String(d.getMinutes()).padStart(2, "0");
return `${hh}:${mm}`;
} catch {
return "";
}
};
const update = () => {
const v = clamp(Math.round(getValue()), 1, getMax());
setValue(v);
const ts = (typeof getTargetTs === "function") ? getTargetTs() : null;
if (ts) setHandsFromTs(ts);
big.textContent = labelFn(v);
if (ts) sub.textContent = `Will remind at: ${fmtTime(ts)}`;
else sub.textContent = `Use ± • max ${getMax()}`;
};
btnMinus.addEventListener("click", () => {
const step = getStep();
setValue(clamp(Math.round(getValue()) - step, 1, getMax()));
update();
});
btnPlus.addEventListener("click", () => {
const step = getStep();
setValue(clamp(Math.round(getValue()) + step, 1, getMax()));
update();
});
update();
return { wrap, update };
};
const makeAtPreviewClock = ({ getDate, getTime, getTargetTs, setTime }) => {
const wrap = document.createElement("div");
wrap.className = "wmeRcClockWrap";
const clock = document.createElement("div");
clock.className = "wmeRcClock";
const ticks = document.createElement("div");
ticks.className = "wmeRcClockTicks";
for (let i = 0; i < 12; i++) {
const t = document.createElement("div");
t.className = "wmeRcClockTick";
t.style.transform = `translate(-50%,-68px) rotate(${i * 30}deg) translate(0, 68px)`;
ticks.appendChild(t);
}
const nums = document.createElement("div");
nums.className = "wmeRcClockNums";
for (let n = 1; n <= 12; n++) {
const sp = document.createElement("div");
sp.className = "wmeRcClockNum";
sp.textContent = String(n === 12 ? 12 : n);
const ang = ((n % 12) / 12) * Math.PI * 2 - (Math.PI / 2);
const r = 52;
const x = Math.cos(ang) * r;
const y = Math.sin(ang) * r;
sp.style.transform = `translate(-50%,-50%) translate(${x.toFixed(1)}px, ${y.toFixed(1)}px)`;
nums.appendChild(sp);
}
const handMin = document.createElement("div");
handMin.className = "wmeRcClockHand min";
const handHour = document.createElement("div");
handHour.className = "wmeRcClockHand hour";
const handSec = document.createElement("div");
handSec.className = "wmeRcClockHand sec";
const center = document.createElement("div");
center.className = "wmeRcClockCenter";
clock.appendChild(ticks);
clock.appendChild(nums);
clock.appendChild(handHour);
clock.appendChild(handSec);
clock.appendChild(handMin);
clock.appendChild(center);
try {
clock.style.touchAction = "none";
handMin.style.cursor = "grab";
let dragging = false;
let dragPid = null;
const timeToParts = (t) => {
const m = String(t || "").match(/^(\d{2}):(\d{2})$/);
if (!m) return { hh: 0, mm: 0 };
return { hh: Math.max(0, Math.min(23, parseInt(m[1], 10))), mm: Math.max(0, Math.min(59, parseInt(m[2], 10))) };
};
const setMinuteFromClient = (clientX, clientY) => {
const r = clock.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = clientX - cx;
const dy = clientY - cy;
let ang = Math.atan2(dy, dx); // 0 at +x, CCW
let deg = (ang * 180 / Math.PI) + 90;
deg = (deg + 360) % 360;
const minute = Math.round(deg / 6) % 60;
const cur = timeToParts(getTime());
const hh = cur.hh;
const mm = minute;
const next = `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
if (typeof setTime === "function") setTime(next);
update();
};
const onMove = (ev) => {
if (!dragging) return;
if (dragPid != null && ev.pointerId != null && ev.pointerId !== dragPid) return;
try { ev.preventDefault(); } catch {}
setMinuteFromClient(ev.clientX, ev.clientY);
};
const endDrag = (ev) => {
if (dragPid != null && ev.pointerId != null && ev.pointerId !== dragPid) return;
dragging = false;
dragPid = null;
try { handMin.releasePointerCapture(ev.pointerId); } catch {}
try { window.removeEventListener("pointermove", onMove, true); } catch {}
try { window.removeEventListener("pointerup", endDrag, true); } catch {}
try { window.removeEventListener("pointercancel", endDrag, true); } catch {}
try { handMin.style.cursor = "grab"; } catch {}
};
handMin.addEventListener("pointerdown", (ev) => {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
dragging = true;
dragPid = ev.pointerId;
try { handMin.setPointerCapture(ev.pointerId); } catch {}
try { handMin.style.cursor = "grabbing"; } catch {}
setMinuteFromClient(ev.clientX, ev.clientY);
window.addEventListener("pointermove", onMove, true);
window.addEventListener("pointerup", endDrag, true);
window.addEventListener("pointercancel", endDrag, true);
}, true);
} catch {}
const valueBox = document.createElement("div");
valueBox.className = "wmeRcClockValue";
const big = document.createElement("div");
big.className = "wmeRcBigValue";
const sub = document.createElement("div");
sub.className = "wmeRcBigValueSub";
valueBox.appendChild(big);
valueBox.appendChild(sub);
wrap.appendChild(clock);
wrap.appendChild(valueBox);
let _lastMinDeg = null, _minOffsetDeg = 0;
let _lastHourDeg = null, _hourOffsetDeg = 0;
const _applySmoothDeg = (rawDeg, lastDeg, offsetDeg) => {
let deg = rawDeg + offsetDeg;
if (lastDeg != null) {
if (deg < lastDeg - 180) { offsetDeg += 360; deg = rawDeg + offsetDeg; }
else if (deg > lastDeg + 180) { offsetDeg -= 360; deg = rawDeg + offsetDeg; }
}
return { deg, offsetDeg };
};
const setHandsFromTs = (ts) => {
try {
const d = new Date(ts);
const hh = d.getHours();
const mm = d.getMinutes();
const ss = d.getSeconds();
const h = (hh % 12) + (mm / 60);
const rawH = (h / 12) * 360;
const rawM = (mm / 60) * 360;
const rawS = (ss / 60) * 360;
const hhRes = _applySmoothDeg(rawH, _lastHourDeg, _hourOffsetDeg);
_hourOffsetDeg = hhRes.offsetDeg;
_lastHourDeg = hhRes.deg;
const mmRes = _applySmoothDeg(rawM, _lastMinDeg, _minOffsetDeg);
_minOffsetDeg = mmRes.offsetDeg;
_lastMinDeg = mmRes.deg;
handHour.style.transform = `translate(-50%,-100%) rotate(${hhRes.deg}deg)`;
handMin.style.transform = `translate(-50%,-100%) rotate(${mmRes.deg}deg)`;
try { handSec.style.transform = `translate(-50%,-100%) rotate(${rawS}deg)`; } catch {}
} catch {
_lastMinDeg = null; _minOffsetDeg = 0;
_lastHourDeg = null; _hourOffsetDeg = 0;
handHour.style.transform = `translate(-50%,-100%) rotate(0deg)`;
handMin.style.transform = `translate(-50%,-100%) rotate(0deg)`;
try { handSec.style.transform = `translate(-50%,-100%) rotate(0deg)`; } catch {}
}
};
const fmtTime = (ts) => {
try {
const d = new Date(ts);
const hh = String(d.getHours()).padStart(2, "0");
const mm = String(d.getMinutes()).padStart(2, "0");
return `${hh}:${mm}`;
} catch {
return "";
}
};
const update = () => {
const d = getDate();
const tt = getTime();
big.textContent = `${d} ${tt || "00:00"}`;
const ts = (typeof getTargetTs === "function") ? getTargetTs() : calcAtTargetTs();
if (ts) {
setHandsFromTs(ts);
sub.textContent = `Will remind at: ${fmtTime(ts)} • ${fmtPreview(ts)}`;
} else {
setHandsFromTs(Date.now());
sub.textContent = "Pick a date & time";
}
};
update();
return { wrap, update };
};
openModal({
title: "Reminder",
iconSvg: ICONS.bell,
bodyBuilder: ({ body, close, modal }) => {
try { applyPinMarkerColors(modal, (pin && pin.color) || "#ff8a00"); } catch {}
try { const c = normalizePinColor((pin && pin.color) || "#ff8a00"); const rgb = hexToRgb(c); if (rgb) modal.style.setProperty("--pinRGB", `${rgb.r},${rgb.g},${rgb.b}`); } catch {}
const topRow = document.createElement("div");
topRow.className = "wmeRcWizardHeader";
topRow.innerHTML = `
<div class="wmeRcWizardHdrLeft">
<div class="wmeRcWizardTitle">Set a reminder</div>
<div class="wmeRcWizardSub">${pin.name || "Pinned place"}</div>
</div>
`;
const tabs = document.createElement("div");
tabs.className = "wmeRcTabs";
const tabIn = document.createElement("div");
tabIn.className = "wmeRcTab";
tabIn.textContent = "In";
const tabAt = document.createElement("div");
tabAt.className = "wmeRcTab";
tabAt.textContent = "At";
tabs.appendChild(tabIn);
tabs.appendChild(tabAt);
topRow.appendChild(tabs);
const hint = document.createElement("div");
hint.className = "wmeRcHint";
hint.textContent = prettyCurrent();
let noteVal = String(pin.reminderNote || "");
const noteBox = document.createElement("div");
noteBox.className = "wmeRcWizardSection";
const noteLbl = document.createElement("div");
noteLbl.className = "wmeRcHint";
noteLbl.textContent = "Notes";
const noteInp = document.createElement("textarea");
noteInp.className = "wmeRcInput";
noteInp.placeholder = "Add a note…";
noteInp.value = noteVal;
noteInp.rows = 4;
try { noteInp.style.resize = "none"; } catch {}
try {
noteInp.style.overflowY = "hidden";
noteInp.style.minHeight = "88px";
noteInp.style.maxHeight = "88px";
} catch {}
noteInp.addEventListener("input", () => { noteVal = noteInp.value; });
noteBox.appendChild(noteLbl);
noteBox.appendChild(noteInp);
const section = document.createElement("div");
section.className = "wmeRcWizardSection";
const unitRow = document.createElement("div");
unitRow.className = "wmeRcSplit";
const unitMin = document.createElement("div");
unitMin.className = "wmeRcWizardBtn";
unitMin.textContent = "Minutes";
const unitHr = document.createElement("div");
unitHr.className = "wmeRcWizardBtn";
unitHr.textContent = "Hours";
unitRow.appendChild(unitMin);
unitRow.appendChild(unitHr);
const clockIn = makeClock({
getValue: () => inValue,
setValue: (v) => { inValue = v; },
getMax: () => getMaxForUnit(),
getStep: () => getStepForUnit(),
getTargetTs: () => calcInTargetTs(),
getUnitLabel: () => unit === "HOURS" ? "hours" : "minutes",
labelFn: (v) => unit === "HOURS" ? `${v} hour${v === 1 ? "" : "s"}` : `${v} minute${v === 1 ? "" : "s"}`,
});
const quick = document.createElement("div");
quick.className = "wmeRcQuickRow";
const mkQuick = (label, nextUnit, nextValue) => {
const b = document.createElement("div");
b.className = "wmeRcQuick";
b.textContent = label;
b.addEventListener("click", () => {
unit = nextUnit;
inValue = nextValue;
syncUI();
});
return b;
};
quick.appendChild(mkQuick("2m", "MINUTES", 2));
quick.appendChild(mkQuick("5m", "MINUTES", 5));
quick.appendChild(mkQuick("10m", "MINUTES", 10));
quick.appendChild(mkQuick("15m", "MINUTES", 15));
quick.appendChild(mkQuick("30m", "MINUTES", 30));
quick.appendChild(mkQuick("1h", "HOURS", 1));
quick.appendChild(mkQuick("2h", "HOURS", 2));
quick.appendChild(mkQuick("4h", "HOURS", 4));
const dt = document.createElement("div");
dt.className = "wmeRcDT";
let calPop = null;
let timePop = null;
const fmtDateLabel = () => {
try {
const parts = String(atDate || "").split("-");
if (parts.length !== 3) return "Pick date";
return `${parts[2]}/${parts[1]}/${parts[0]}`;
} catch {
return "Pick date";
}
};
const fmtTimeLabel = () => {
try {
const s = String(atTime || "");
if (!s || s.length < 4) return "Pick time";
return s;
} catch {
return "Pick time";
}
};
const dateBtn = document.createElement("button");
dateBtn.type = "button";
dateBtn.className = "wmeRcPickerBtn";
const timeBtn = document.createElement("button");
timeBtn.type = "button";
timeBtn.className = "wmeRcPickerBtn";
const syncPickers = () => {
try { dateBtn.innerHTML = `<span class="wmeRcPickerK">Date</span><span class="wmeRcPickerV">${fmtDateLabel()}</span>`; } catch {}
try { timeBtn.innerHTML = `<span class="wmeRcPickerK">Time</span><span class="wmeRcPickerV">${fmtTimeLabel()}</span>`; } catch {}
};
syncPickers();
const closePickers = () => {
try {
if (calPop) {
try { document.removeEventListener("pointerdown", calPop._wmeRcDown, true); } catch {}
calPop.remove();
}
} catch {}
try {
if (timePop) {
try { document.removeEventListener("pointerdown", timePop._wmeRcDown, true); } catch {}
timePop.remove();
}
} catch {}
calPop = null;
timePop = null;
};
const posPop = (pop, anchorEl) => {
try {
const r = anchorEl.getBoundingClientRect();
const pw = pop.offsetWidth || 320;
const ph = pop.offsetHeight || 280;
let left = r.left;
left = Math.max(12, Math.min(window.innerWidth - pw - 12, left));
let top = r.bottom + 10;
if (top + ph > window.innerHeight - 12) top = r.top - ph - 10;
top = Math.max(12, Math.min(window.innerHeight - ph - 12, top));
pop.style.left = left + "px";
pop.style.top = top + "px";
} catch {}
};
const openCalendar = () => {
try {
closePickers();
calPop = document.createElement("div");
calPop.className = "wmeRcPickerPop wmeRcCalPop";
try { applyPinMarkerColors(calPop, (pin && pin.color) || "#ff8a00"); } catch {}
try { const c = normalizePinColor((pin && pin.color) || "#ff8a00"); const rgb = hexToRgb(c); if (rgb) calPop.style.setProperty("--pinRGB", `${rgb.r},${rgb.g},${rgb.b}`); } catch {}
const header = document.createElement("div");
header.className = "wmeRcCalHdr";
const btnPrev = document.createElement("button");
btnPrev.type = "button";
btnPrev.className = "wmeRcCalNav";
btnPrev.textContent = "‹";
const btnNext = document.createElement("button");
btnNext.type = "button";
btnNext.className = "wmeRcCalNav";
btnNext.textContent = "›";
const title = document.createElement("div");
title.className = "wmeRcCalTitle";
header.appendChild(btnPrev);
header.appendChild(title);
header.appendChild(btnNext);
const dow = document.createElement("div");
dow.className = "wmeRcCalDow";
for (const d of ["M","T","W","T","F","S","S"]) {
const c = document.createElement("div");
c.textContent = d;
dow.appendChild(c);
}
const grid = document.createElement("div");
grid.className = "wmeRcCalGrid";
const parseYmd = (s) => {
try {
const parts = String(s || "").split("-");
if (parts.length !== 3) return null;
const y = parseInt(parts[0], 10);
const m = parseInt(parts[1], 10);
const d = parseInt(parts[2], 10);
if (!Number.isFinite(y) || !Number.isFinite(m) || !Number.isFinite(d)) return null;
return { y, m, d };
} catch {
return null;
}
};
let view = (() => {
const p = parseYmd(atDate);
if (p) return { y: p.y, m: p.m };
const n = new Date();
return { y: n.getFullYear(), m: n.getMonth() + 1 };
})();
const toYmd = (y, m, d) => `${String(y).padStart(4, "0")}-${String(m).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
const render = () => {
try {
const dt0 = new Date(view.y, view.m - 1, 1);
title.textContent = dt0.toLocaleString(undefined, { month: "long", year: "numeric" });
} catch {
title.textContent = `${view.m}/${view.y}`;
}
grid.innerHTML = "";
const first = new Date(view.y, view.m - 1, 1);
const firstDow = (first.getDay() + 6) % 7; // monday=0
const daysIn = new Date(view.y, view.m, 0).getDate();
for (let i = 0; i < firstDow; i++) {
const off = document.createElement("div");
off.className = "wmeRcCalCell off";
grid.appendChild(off);
}
const sel = parseYmd(atDate);
for (let d = 1; d <= daysIn; d++) {
const b = document.createElement("button");
b.type = "button";
b.className = "wmeRcCalCell";
b.textContent = String(d);
if (sel && sel.y === view.y && sel.m === view.m && sel.d === d) b.classList.add("sel");
b.addEventListener("click", (ev) => {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
atDate = toYmd(view.y, view.m, d);
syncPickers();
try { clockAt.update(); } catch {}
closePickers();
});
grid.appendChild(b);
}
};
btnPrev.addEventListener("click", (ev) => {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
view.m -= 1;
if (view.m < 1) { view.m = 12; view.y -= 1; }
render();
});
btnNext.addEventListener("click", (ev) => {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
view.m += 1;
if (view.m > 12) { view.m = 1; view.y += 1; }
render();
});
calPop.appendChild(header);
calPop.appendChild(dow);
calPop.appendChild(grid);
(document.body || document.documentElement).appendChild(calPop);
render();
requestAnimationFrame(() => posPop(calPop, dateBtn));
const onDown = (ev) => {
try {
const t = ev.target;
if (!calPop) return;
if (t && (calPop.contains(t) || dateBtn.contains(t))) return;
closePickers();
} catch {}
};
document.addEventListener("pointerdown", onDown, true);
try { calPop._wmeRcDown = onDown; } catch {}
} catch {}
};
const openTime = () => {
try {
closePickers();
timePop = document.createElement("div");
timePop.className = "wmeRcPickerPop wmeRcTimePop";
try { applyPinMarkerColors(timePop, (pin && pin.color) || "#ff8a00"); } catch {}
const header = document.createElement("div");
header.className = "wmeRcTimeHdr";
header.textContent = "Pick time";
const box = document.createElement("div");
box.className = "wmeRcTimeClockBox";
const label = document.createElement("input");
label.type = "text";
label.inputMode = "numeric";
label.autocomplete = "off";
label.spellcheck = false;
label.maxLength = 5;
label.className = "wmeRcTimeClockLabel wmeRcTimeClockInput";
label.value = atTime || "00:00";
const help = document.createElement("div");
help.className = "wmeRcTimeClockHelp";
help.textContent = "Drag the clock hands"; // Auto hand selection (no Hour/Minute toggle)
let pickHandFromClient = () => "MIN";
const clock = document.createElement("div");
clock.className = "wmeRcClock wmeRcClockPick";
pickHandFromClient = (clientX, clientY) => {
try {
const r = clock.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = clientX - cx;
const dy = clientY - cy;
const dist = Math.sqrt(dx*dx + dy*dy);
const radius = Math.min(r.width, r.height) / 2;
return (dist < radius * 0.56) ? "HOUR" : "MIN";
} catch {
return "MIN";
}
};
const ticks = document.createElement("div");
ticks.className = "wmeRcClockTicks";
for (let i = 0; i < 12; i++) {
const t = document.createElement("div");
t.className = "wmeRcClockTick";
t.style.transform = `translate(-50%,-78px) rotate(${i * 30}deg) translate(0, 78px)`;
ticks.appendChild(t);
}
const nums = document.createElement("div");
nums.className = "wmeRcClockNums";
for (let n = 1; n <= 12; n++) {
const sp = document.createElement("div");
sp.className = "wmeRcClockNum";
sp.textContent = String(n === 12 ? 12 : n);
const ang = ((n % 12) / 12) * Math.PI * 2 - (Math.PI / 2);
const r = 60;
const x = Math.cos(ang) * r;
const y = Math.sin(ang) * r;
sp.style.transform = `translate(-50%,-50%) translate(${x.toFixed(1)}px, ${y.toFixed(1)}px)`;
nums.appendChild(sp);
}
const handMin = document.createElement("div");
handMin.className = "wmeRcClockHand min";
handMin.style.pointerEvents = "auto";
handMin.style.cursor = "grab";
const handHour = document.createElement("div");
handHour.className = "wmeRcClockHand hour";
handHour.style.pointerEvents = "auto";
handHour.style.cursor = "grab";
const center = document.createElement("div");
center.className = "wmeRcClockCenter";
clock.appendChild(ticks);
clock.appendChild(nums);
clock.appendChild(handHour);
clock.appendChild(handMin);
clock.appendChild(center);
const timeToParts = (t) => {
const m = String(t || "").match(/^(\d{2}):(\d{2})$/);
if (!m) return { hh: 0, mm: 0 };
return { hh: Math.max(0, Math.min(23, parseInt(m[1], 10))), mm: Math.max(0, Math.min(59, parseInt(m[2], 10))) };
};
const normalizeTimeStr = (s) => {
try {
let v = String(s || "").trim();
if (!v) return null;
v = v.replace(/\s+/g, "");
let m = v.match(/^(\d{1,2}):(\d{2})$/);
if (m) {
const hh = Math.max(0, Math.min(23, parseInt(m[1], 10)));
const mm = Math.max(0, Math.min(59, parseInt(m[2], 10)));
return `${String(hh).padStart(2,"0")}:${String(mm).padStart(2,"0")}`;
}
m = v.match(/^(\d{3,4})$/);
if (m) {
const raw = m[1].padStart(4, "0");
const hh = Math.max(0, Math.min(23, parseInt(raw.slice(0,2), 10)));
const mm = Math.max(0, Math.min(59, parseInt(raw.slice(2,4), 10)));
return `${String(hh).padStart(2,"0")}:${String(mm).padStart(2,"0")}`;
}
return null;
} catch {
return null;
}
};
const applyTypedTime = (closeAfter) => {
try {
const norm = normalizeTimeStr(label.value);
if (norm) {
atTime = norm;
syncPickers();
try { clockAt.update(); } catch {}
updateHands();
} else {
try { label.value = atTime || "00:00"; } catch {}
}
} catch {}
if (closeAfter) { try { closePickers(); } catch {} }
};
try {
label.style.color = "#fff";
label.style.caretColor = "#fff";
} catch {}
let _editHH = "00";
let _editMM = "00";
let _editPhase = "H"; // H or M
let _editIdx = 0;
const _syncEditFromAt = () => {
const p = timeToParts(atTime || "00:00");
_editHH = String(p.hh).padStart(2, "0");
_editMM = String(p.mm).padStart(2, "0");
};
const _renderEdit = () => {
try { label.value = `${_editHH}:${_editMM}`; } catch {}
};
const _applyEditToAt = () => {
try {
let hh = parseInt(_editHH, 10);
let mm = parseInt(_editMM, 10);
if (Number.isNaN(hh)) hh = 0;
if (Number.isNaN(mm)) mm = 0;
hh = Math.max(0, Math.min(23, hh));
mm = Math.max(0, Math.min(59, mm));
_editHH = String(hh).padStart(2, "0");
_editMM = String(mm).padStart(2, "0");
atTime = `${_editHH}:${_editMM}`;
syncPickers();
try { clockAt.update(); } catch {}
updateHands();
_renderEdit();
} catch {}
};
const _selectHours = () => { try { label.setSelectionRange(0, 2); } catch {} _editPhase = "H"; _editIdx = 0; };
const _selectMinutes = () => { try { label.setSelectionRange(3, 5); } catch {} _editPhase = "M"; _editIdx = 0; };
label.addEventListener("focus", () => {
_syncEditFromAt();
_renderEdit();
_selectHours();
});
label.addEventListener("pointerup", () => {
try {
const s = label.selectionStart || 0;
if (s >= 3) { _editPhase = "M"; _editIdx = Math.min(1, Math.max(0, s - 3)); }
else { _editPhase = "H"; _editIdx = Math.min(1, Math.max(0, s)); }
} catch {}
});
label.addEventListener("paste", (ev) => {
try {
const txt = (ev.clipboardData && ev.clipboardData.getData("text")) || "";
const norm = normalizeTimeStr(txt);
if (norm) {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
atTime = norm;
_syncEditFromAt();
_renderEdit();
_applyEditToAt();
_selectMinutes();
}
} catch {}
});
label.addEventListener("keydown", (ev) => {
if (!ev) return;
const k = ev.key;
if (k === "Escape") { try { ev.preventDefault(); ev.stopPropagation(); } catch {} closePickers(); return; }
if (k === "Enter") { try { ev.preventDefault(); ev.stopPropagation(); } catch {} _applyEditToAt(); closePickers(); return; }
if (k === ":") { try { ev.preventDefault(); ev.stopPropagation(); } catch {} _selectMinutes(); return; }
if (/^[0-9]$/.test(k)) {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
_syncEditFromAt();
if (_editPhase === "H") {
_editHH = (_editIdx === 0) ? (k + _editHH[1]) : (_editHH[0] + k);
_editIdx++;
_applyEditToAt();
if (_editIdx >= 2) { _selectMinutes(); return; }
try { label.setSelectionRange(_editIdx, _editIdx); } catch {}
return;
}
_editMM = (_editIdx === 0) ? (k + _editMM[1]) : (_editMM[0] + k);
_editIdx++;
_applyEditToAt();
if (_editIdx >= 2) { try { label.setSelectionRange(5, 5); } catch {} return; }
try { label.setSelectionRange(3 + _editIdx, 3 + _editIdx); } catch {}
return;
}
if (k === "Backspace") {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
_syncEditFromAt();
if (_editPhase === "M") {
if (_editIdx <= 0) { _selectHours(); try { label.setSelectionRange(1, 1); } catch {} _editIdx = 1; return; }
_editIdx--;
_editMM = (_editIdx === 0) ? ("0" + _editMM[1]) : (_editMM[0] + "0");
_applyEditToAt();
try { label.setSelectionRange(3 + _editIdx, 3 + _editIdx); } catch {}
return;
}
if (_editIdx <= 0) { _editIdx = 0; _editHH = "00"; _applyEditToAt(); _selectHours(); return; }
_editIdx--;
_editHH = (_editIdx === 0) ? ("0" + _editHH[1]) : (_editHH[0] + "0");
_applyEditToAt();
try { label.setSelectionRange(_editIdx, _editIdx); } catch {}
return;
}
if (k === "Delete") {
try {
const s = label.selectionStart || 0;
if (s === 2) { ev.preventDefault(); ev.stopPropagation(); _selectMinutes(); return; }
} catch {}
}
});
label.addEventListener("blur", () => { try { _applyEditToAt(); } catch {} });
const cur = timeToParts(atTime || "00:00");
const setHm = (hh, mm) => {
atTime = `${String(hh).padStart(2, "0")}:${String(mm).padStart(2, "0")}`;
syncPickers();
try { clockAt.update(); } catch {}
updateHands();
};
const updateLabel = () => { try { label.value = atTime || "00:00"; } catch {} };
const updateHands = () => {
const p = timeToParts(atTime || "00:00");
const hh = p.hh;
const mm = p.mm;
const h = (hh % 12) + (mm / 60);
const hDeg = (h / 12) * 360;
const mDeg = (mm / 60) * 360;
handHour.style.transform = `translate(-50%,-100%) rotate(${hDeg}deg)`;
handMin.style.transform = `translate(-50%,-100%) rotate(${mDeg}deg)`;
updateLabel();
};
const angleFromClient = (clientX, clientY) => {
const r = clock.getBoundingClientRect();
const cx = r.left + r.width / 2;
const cy = r.top + r.height / 2;
const dx = clientX - cx;
const dy = clientY - cy;
let ang = Math.atan2(dy, dx);
let deg = (ang * 180 / Math.PI) + 90;
deg = (deg + 360) % 360;
return deg;
};
const setMinuteFromClient = (clientX, clientY) => {
const deg = angleFromClient(clientX, clientY);
const minute = Math.round(deg / 6) % 60;
const p = timeToParts(atTime || "00:00");
setHm(p.hh, minute);
};
const setHourFromClient = (clientX, clientY) => {
const deg = angleFromClient(clientX, clientY);
const h12 = Math.round(deg / 30) % 12; // 0..11, where 0=12
const p = timeToParts(atTime || "00:00");
const curH = p.hh;
const candA = (h12 === 0 ? 12 : h12) % 12; // 0..11
const a = candA;
const b = candA + 12;
const pick = (Math.abs(a - curH) <= Math.abs(b - curH)) ? a : b;
setHm(Math.max(0, Math.min(23, pick)), p.mm);
};
let _dragLastDeg = null;
const startDrag = (ev, forcedMode) => {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
const pid = ev.pointerId;
try { clock.setPointerCapture(pid); } catch {}
const dragMode = (() => {
try {
if (forcedMode) return forcedMode;
const t = ev.target;
if (t === handHour) return "HOUR";
if (t === handMin) return "MIN";
return pickHandFromClient(ev.clientX, ev.clientY);
} catch {
return "MIN";
}
})();
_dragLastDeg = null;
try {
if (dragMode === "HOUR") {
handHour.style.cursor = "grabbing";
handMin.style.cursor = "default";
} else {
handMin.style.cursor = "grabbing";
handHour.style.cursor = "default";
}
} catch {}
const apply = (x, y) => {
if (dragMode === "HOUR") {
setHourFromClient(x, y);
return;
}
const deg = angleFromClient(x, y);
const minute = Math.round(deg / 6) % 60;
const p = timeToParts(atTime || "00:00");
let hh = p.hh;
const prevMin = p.mm;
if (_dragLastDeg != null) {
const delta = ((deg - _dragLastDeg + 540) % 360) - 180; // signed shortest step
if (delta > 0 && prevMin >= 50 && minute <= 10) hh = (hh + 1) % 24;
if (delta < 0 && prevMin <= 10 && minute >= 50) hh = (hh + 23) % 24;
}
_dragLastDeg = deg;
setHm(hh, minute);
};
const onMove = (e) => {
if (e.pointerId != null && e.pointerId !== pid) return;
try { e.preventDefault(); } catch {}
apply(e.clientX, e.clientY);
};
const end = (e) => {
if (e.pointerId != null && e.pointerId !== pid) return;
try { clock.releasePointerCapture(pid); } catch {}
try { window.removeEventListener("pointermove", onMove, true); } catch {}
try { window.removeEventListener("pointerup", end, true); } catch {}
try { window.removeEventListener("pointercancel", end, true); } catch {}
try {
handMin.style.cursor = "grab";
handHour.style.cursor = "grab";
} catch {}
_dragLastDeg = null;
};
apply(ev.clientX, ev.clientY);
window.addEventListener("pointermove", onMove, true);
window.addEventListener("pointerup", end, true);
window.addEventListener("pointercancel", end, true);
};
handMin.addEventListener("pointerdown", (ev) => startDrag(ev, "MIN"), true);
handHour.addEventListener("pointerdown", (ev) => startDrag(ev, "HOUR"), true);
clock.addEventListener("pointerdown", (ev) => {
try {
const t = ev.target;
if (t === handMin || t === handHour) return;
} catch {}
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
const mode = pickHandFromClient(ev.clientX, ev.clientY);
startDrag(ev, mode);
}, true);
const doneRow = document.createElement("div");
doneRow.className = "wmeRcTimeDoneRow";
const resetBtn = document.createElement("button");
resetBtn.type = "button";
resetBtn.className = "wmeRcTimeDoneBtn wmeRcTimeResetBtn";
resetBtn.textContent = "Reset time";
resetBtn.addEventListener("click", (ev) => {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
try {
const d = new Date();
setHm(d.getHours(), d.getMinutes());
try { _syncEditFromAt(); _renderEdit(); } catch {}
try { label.focus(); _selectHours(); } catch {}
} catch {}
});
const doneBtn = document.createElement("button");
doneBtn.type = "button";
doneBtn.className = "wmeRcTimeDoneBtn";
doneBtn.textContent = "Save";
doneBtn.addEventListener("click", (ev) => { try { ev.preventDefault(); ev.stopPropagation(); } catch {} closePickers(); });
doneRow.appendChild(resetBtn);
doneRow.appendChild(doneBtn);
box.appendChild(label);
box.appendChild(clock);
box.appendChild(help);
box.appendChild(doneRow);
timePop.appendChild(header);
timePop.appendChild(box);
(document.body || document.documentElement).appendChild(timePop);
updateHands();
requestAnimationFrame(() => posPop(timePop, timeBtn));
timePop.addEventListener("keydown", (ev) => {
if (ev.key === "Enter") { try { ev.preventDefault(); ev.stopPropagation(); } catch {} closePickers(); }
});
const onDown = (ev) => {
try {
const t = ev.target;
if (!timePop) return;
if (t && (timePop.contains(t) || timeBtn.contains(t))) return;
closePickers();
} catch {}
};
document.addEventListener("pointerdown", onDown, true);
try { timePop._wmeRcDown = onDown; } catch {}
} catch {}
};
dateBtn.addEventListener("click", (ev) => { try { ev.preventDefault(); ev.stopPropagation(); } catch {} openCalendar(); });
timeBtn.addEventListener("click", (ev) => { try { ev.preventDefault(); ev.stopPropagation(); } catch {} openTime(); });
dt.appendChild(dateBtn);
dt.appendChild(timeBtn);
const clockAt = makeAtPreviewClock({
getDate: () => atDate,
getTime: () => atTime,
getTargetTs: () => calcAtTargetTs(),
setTime: (t) => { atTime = String(t || atTime); try { syncPickers(); } catch {} },
});
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const btnCancel = document.createElement("div");
btnCancel.className = "wmeRcModalBtn";
btnCancel.textContent = "Cancel";
btnCancel.addEventListener("click", () => { try { if (typeof cleanupSoundPick === "function") cleanupSoundPick(); } catch {} close(); });
const btnClear = document.createElement("div");
btnClear.className = "wmeRcModalBtn danger";
btnClear.textContent = "Clear";
btnClear.addEventListener("click", () => {
updatePin(pinId, { reminderAt: null, reminderType: null, reminderUnit: null, reminderValue: null, reminderDone: false, reminderNote: "" });
toast("Reminder cleared");
close();
});
const btnSet = document.createElement("div");
btnSet.className = "wmeRcModalBtn primary";
btnSet.textContent = "Set";
btnSet.addEventListener("click", async () => {
let when = null;
if (mode === "IN") {
when = calcInTargetTs();
updatePin(pinId, { reminderAt: when, reminderType: "IN", reminderUnit: unit, reminderValue: Math.max(1, Math.round(inValue)), reminderDone: false, reminderNote: String(noteVal||"").trim() });
} else {
const ts = calcAtTargetTs();
if (!ts) { toast("Pick a valid date & time"); return; }
if (ts <= Date.now() + 5000) { toast("Pick a time in the future"); return; }
when = ts;
updatePin(pinId, { reminderAt: when, reminderType: "AT", reminderUnit: null, reminderValue: null, reminderDone: false, reminderNote: String(noteVal||"").trim() });
}
toast("Reminder set");
await ensureNotificationPermission();
renderPinsPanel();
close();
});
actions.appendChild(btnCancel);
actions.appendChild(btnClear);
actions.appendChild(btnSet);
const syncUI = () => {
tabIn.classList.toggle("on", mode === "IN");
tabAt.classList.toggle("on", mode === "AT");
unitMin.classList.toggle("on", unit === "MINUTES");
unitHr.classList.toggle("on", unit === "HOURS");
inValue = clamp(Math.round(inValue), 1, getMaxForUnit());
section.innerHTML = "";
if (mode === "IN") {
section.appendChild(unitRow);
section.appendChild(clockIn.wrap);
section.appendChild(quick);
clockIn.update();
} else {
section.appendChild(dt);
section.appendChild(clockAt.wrap);
clockAt.update();
}
try {
const ts = mode === "IN" ? calcInTargetTs() : calcAtTargetTs();
if (ts) hint.textContent = `Will remind at: ${fmtPreview(ts)} • ${prettyCurrent()}`;
else hint.textContent = prettyCurrent();
} catch {
hint.textContent = prettyCurrent();
}
};
tabIn.addEventListener("click", () => { mode = "IN"; syncUI(); });
tabAt.addEventListener("click", () => { mode = "AT"; syncUI(); });
unitMin.addEventListener("click", () => { unit = "MINUTES"; syncUI(); });
unitHr.addEventListener("click", () => { unit = "HOURS"; syncUI(); });
body.appendChild(topRow);
body.appendChild(section);
body.appendChild(noteBox);
body.appendChild(actions);
syncUI();
},
});
}
async function ensureNotificationPermission() {
try {
if (!("Notification" in window)) return false;
if (Notification.permission === "granted") return true;
if (Notification.permission === "denied") return false;
const res = await Notification.requestPermission();
return res === "granted";
} catch {
return false;
}
}
function showReminderNotice(pinsDue, opts) {
try {
ensureCSS();
const _opts = (opts && typeof opts === "object") ? opts : {};
const _titleText = typeof _opts.title === "string" && _opts.title.trim() ? _opts.title.trim() : "Reminder";
const _silent = !!_opts.silent;
const pinsList = Array.isArray(pinsDue) ? pinsDue.filter(Boolean) : (pinsDue ? [pinsDue] : []);
if (!pinsList.length) return false;
let stack = document.getElementById("wmeRcNoticeStack");
if (!stack) {
stack = document.createElement("div");
stack.id = "wmeRcNoticeStack";
stack.className = "wmeRcNoticeStack";
(document.body || document.documentElement).appendChild(stack);
}
try { applyReminderNoticePosition(); } catch {}
const createNotice = (pin) => {
const primaryPin = pin;
const msgText = primaryPin?.name ? String(primaryPin.name) : "Pinned place";
const wrap = document.createElement("div");
wrap.className = "wmeRcNotice";
activeReminderNotices.add(wrap);
const title = document.createElement("div");
title.className = "wmeRcNoticeTitle";
title.textContent = _titleText;
const msg = document.createElement("div");
msg.className = "wmeRcNoticeMsg";
msg.textContent = msgText;
const noteText = (primaryPin && typeof primaryPin.reminderNote === "string") ? primaryPin.reminderNote.trim() : "";
const note = document.createElement("div");
note.className = "wmeRcNoticeNote";
if (noteText) note.textContent = noteText;
const actions = document.createElement("div");
actions.className = "wmeRcNoticeActions";
const btnSnooze = document.createElement("button");
btnSnooze.className = "wmeRcNoticeBtn";
btnSnooze.textContent = "Snooze";
const btnCancel = document.createElement("button");
btnCancel.className = "wmeRcNoticeBtn";
btnCancel.textContent = "Dismiss";
const btnGo = document.createElement("button");
btnGo.className = "wmeRcNoticeBtn primary";
btnGo.textContent = "Take me there";
try {
const col = normalizePinColor(primaryPin && primaryPin.color ? primaryPin.color : "#ff8a00");
const base = hexToRgb(col);
const light = mixRgb(base, { r:255, g:255, b:255 }, 0.25);
btnGo.style.background = `linear-gradient(180deg, ${rgbaStr(light, .32)}, ${rgbaStr(base, .28)})`;
btnGo.style.borderColor = `${rgbaStr(base, .38)}`;
btnGo.style.boxShadow = `0 10px 28px ${rgbaStr(base, .22)}`;
} catch {}
let pop = null;
const cleanupPop = () => {
try {
if (pop) {
try { window.removeEventListener("resize", pop._wmeRcPos); } catch {}
try { document.removeEventListener("pointerdown", pop._wmeRcDown, true); } catch {}
pop.remove();
}
} catch {}
pop = null;
};
const maybeStopBell = () => {
try {
if (!activeReminderNotices || activeReminderNotices.size === 0) stopBellLoop();
} catch {}
};
const close = () => {
cleanupPop();
try { wrap.classList.remove("show"); } catch {}
setTimeout(() => {
try { wrap.remove(); } catch {}
try { activeReminderNotices.delete(wrap); } catch {}
maybeStopBell();
}, 220);
};
const snoozePin = (minutes) => {
const mins = Math.max(1, Math.min(240, Number(minutes) || 0));
if (!mins) return;
try {
const now = Date.now();
const all = loadPins();
let changed = false;
for (const p of all) {
if (!p || String(p.id) !== String(primaryPin.id)) continue;
p.reminderAt = now + mins * 60000;
p.reminderDone = false;
p.reminderFiredAt = null;
clearFiredKeysForPin(p.id);
changed = true;
break;
}
if (changed) {
savePins(all);
renderPinsPanel();
try { scheduleAllReminderTimers(); } catch {}
}
} catch {}
close();
};
const openSnoozePop = () => {
try { if (pop) pop.remove(); } catch {}
pop = document.createElement("div");
pop.className = "wmeRcSnoozePop";
try { applyPinMarkerColors(pop, (pin && pin.color) || "#ff8a00"); } catch {}
try { const c = normalizePinColor((pin && pin.color) || "#ff8a00"); const rgb = hexToRgb(c); if (rgb) pop.style.setProperty("--pinRGB", `${rgb.r},${rgb.g},${rgb.b}`); } catch {}
const t = document.createElement("div");
t.className = "wmeRcSnoozeTitle";
t.textContent = "Snooze for";
const grid = document.createElement("div");
grid.className = "wmeRcSnoozeGrid";
const presets = [2,5,10,15,30];
for (const m of presets) {
const c = document.createElement("div");
c.className = "wmeRcSnoozeChip";
c.textContent = `${m}m`;
c.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
snoozePin(m);
});
grid.appendChild(c);
}
const row = document.createElement("div");
row.className = "wmeRcSnoozeRow";
const inp = document.createElement("input");
inp.className = "wmeRcSnoozeInput";
inp.type = "text";
inp.inputMode = "numeric";
inp.autocomplete = "off";
inp.maxLength = 3;
inp.placeholder = "Minutes…";
inp.addEventListener("input", () => {
try { inp.value = (inp.value || "").replace(/\D+/g, "").slice(0, 3); } catch {}
});
const go = document.createElement("button");
go.className = "wmeRcSnoozeGo";
go.textContent = "Set";
try {
const col = normalizePinColor((pin && pin.color) ? pin.color : "#ff8a00");
const base = hexToRgb(col);
const light = mixRgb(base, { r:255, g:255, b:255 }, 0.25);
go.style.background = `linear-gradient(180deg, ${rgbaStr(light, .32)}, ${rgbaStr(base, .28)})`;
go.style.borderColor = `${rgbaStr(base, .38)}`;
go.style.boxShadow = `0 10px 28px ${rgbaStr(base, .22)}`;
} catch {}
const commit = () => {
const v = Number(inp.value);
if (!v || v < 1) return;
snoozePin(v);
};
go.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); commit(); });
inp.addEventListener("keydown", (e) => { if (e.key === "Enter") { e.preventDefault(); e.stopPropagation(); commit(); } });
row.appendChild(inp);
row.appendChild(go);
pop.appendChild(t);
pop.appendChild(grid);
pop.appendChild(row);
(document.body || document.documentElement).appendChild(pop);
const positionPop = () => {
try {
const r = wrap.getBoundingClientRect();
const pw = pop.offsetWidth || 280;
const ph = pop.offsetHeight || 220;
let left = (r.left + r.width) - pw - 12;
left = Math.max(12, Math.min(window.innerWidth - pw - 12, left));
let top = r.bottom + 10;
if (top + ph > window.innerHeight - 12) top = r.top - ph - 10;
top = Math.max(12, Math.min(window.innerHeight - ph - 12, top));
pop.style.left = left + "px";
pop.style.top = top + "px";
} catch {}
};
requestAnimationFrame(positionPop);
window.addEventListener("resize", positionPop);
try { pop._wmeRcPos = positionPop; } catch {}
const onDocDown = (ev) => {
try {
const t = ev.target;
if (!pop) return;
if (t && (pop.contains(t) || btnSnooze.contains(t))) return;
pop.remove();
pop = null;
window.removeEventListener("resize", positionPop);
document.removeEventListener("pointerdown", onDocDown, true);
} catch {}
};
document.addEventListener("pointerdown", onDocDown, true);
try { pop._wmeRcDown = onDocDown; } catch {}
setTimeout(() => { try { inp.focus(); } catch {} }, 0);
};
btnSnooze.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
openSnoozePop();
});
btnGo.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
try {
if (primaryPin) zoomToLonLatExact(primaryPin.lon, primaryPin.lat, primaryPin.zoom);
} catch {}
close();
});
btnCancel.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
close();
});
actions.appendChild(btnSnooze);
actions.appendChild(btnCancel);
actions.appendChild(btnGo);
wrap.appendChild(title);
wrap.appendChild(msg);
if (noteText) wrap.appendChild(note);
wrap.appendChild(actions);
stack.appendChild(wrap);
requestAnimationFrame(() => wrap.classList.add("show"));
if (!_silent) startBellLoop();
wrap.addEventListener("click", (e) => {
if (!pop) return;
const target = e.target;
if (target && pop.contains(target)) return;
try { pop.remove(); } catch {}
pop = null;
});
};
for (const p of pinsList) createNotice(p);
return true;
} catch {
return false;
}
}
function fireReminder(pin) {
try { showReminderNotice(pin); } catch {}
}
function fireReminderBatch(pinsDue) {
try {
if (!Array.isArray(pinsDue) || pinsDue.length === 0) return;
showReminderNotice(pinsDue);
} catch {}
}
function checkRemindersNow() {
try {
const now = Date.now();
const pins = loadPins();
const due = [];
for (const p of pins) {
if (!p || !p.id) continue;
const atRaw = p.reminderAt;
if (atRaw == null) continue;
const at = Number(atRaw);
if (!Number.isFinite(at) || at <= 0) continue;
if (p.reminderDone) continue;
if (at <= now) {
due.push(p);
}
}
if (due.length) {
for (const p of due) {
try { triggerReminderById(String(p.id), Number(p.reminderAt)); } catch {}
}
return;
}
try { updatePinsCountdowns(); } catch {}
} catch(e) {
try { console.error("[WME Pins] checkRemindersNow failed", e); } catch {}
}
}
function handleMissedRemindersOnStart(){
try{
const now = Date.now();
const pins = loadPins();
if (!Array.isArray(pins) || pins.length === 0) return;
const missed = [];
const GRACE_MS = 2000;
let changed = false;
for (const p of pins){
if (!p || !p.id) continue;
const atRaw = p.reminderAt;
if (atRaw == null) continue;
const at = Number(atRaw);
if (!Number.isFinite(at) || at <= 0) continue;
if (p.reminderDone) continue;
if (at <= (now - GRACE_MS)){
missed.push(p);
p.reminderDone = true;
p.reminderFiredAt = now;
p.reminderMissed = true;
changed = true;
try { clearReminderTimer(p.id); } catch {}
}
}
if (changed) {
savePins(pins);
try { renderPinsPanel(); } catch {}
}
if (missed.length){
try { showReminderNotice(missed, { title: "Missed reminder", silent: true }); } catch {}
}
}catch{}
}
function startReminderLoop() {
if (reminderIntervalId) return;
if (!missedRemindersChecked) {
missedRemindersChecked = true;
try { handleMissedRemindersOnStart(); } catch {}
}
// Adaptive loop: slows down when tab is hidden to reduce CPU, but catches up immediately on focus/visible.
reminderIntervalId = _createAdaptiveLoop(() => { try { checkRemindersNow(); } catch {} }, 1500);
try { checkRemindersNow(); } catch {}
try { scheduleAllReminderTimers(); } catch {}
if (!startReminderLoop._bound) {
startReminderLoop._bound = true;
try {
window.addEventListener("focus", () => {
try { checkRemindersNow(); scheduleAllReminderTimers(); } catch {}
}, { passive: true });
} catch {}
try {
document.addEventListener("visibilitychange", () => {
if (!document.hidden) {
try { checkRemindersNow(); scheduleAllReminderTimers(); } catch {}
}
}, { passive: true });
} catch {}
}
}
startReminderLoop._bound = false;
function pinThisPlace(kind, ll, segIds) {
if (!ll || !Number.isFinite(ll.lat) || !Number.isFinite(ll.lon)) {
toast("Pin: missing coordinates");
return;
}
ensurePinsPanel();
const pins = loadPins();
let defaultName = `Pinned ${fmt(ll.lat)}, ${fmt(ll.lon)}`;
try {
if (kind === "segment" && Array.isArray(segIds) && segIds.length) {
const sc = getStreetAndCityForSegmentId(segIds[0]);
defaultName = `${sc.street}, ${sc.city}`;
if (segIds.length > 1) defaultName += ` (+${segIds.length - 1})`;
}
} catch {}
const zoom = getCurrentZoomBestEffort();
openModal({
title: "Pin this place",
iconSvg: ICONS.mapPin,
bodyBuilder: ({ body, close, modal }) => {
const nameInp = document.createElement("input");
nameInp.className = "wmeRcInput";
const nextNum = getNextPinNumber(pins);
const fallbackName = `Pin #${nextNum}`;
nameInp.placeholder = fallbackName;
nameInp.value = "";
const colorRow = document.createElement("div");
colorRow.className = "wmeRcColorRow";
let chosenColor = pickRandomPinColor();
let customColors = loadCustomPinColors();
let editCustomIndex = null;
const swatches = [];
function setColor(hex){
chosenColor = normalizePinColor(hex);
for (const s of swatches) s.classList.toggle("sel", s.dataset.c === chosenColor);
try { colorRow.style.setProperty("--pinC", chosenColor); } catch {}
}
let swatchEditTipEl = null;
let swatchEditTipHideT = null;
let swatchEditAnchor = null;
let swatchEditIndex = null;
function ensureSwatchEditTip(){
if (swatchEditTipEl) return;
swatchEditTipEl = document.createElement("div");
swatchEditTipEl.className = "wmeRcSwatchEditTip";
swatchEditTipEl.style.display = "none";
swatchEditTipEl.innerHTML = `
<button type="button" class="wmeRcSwatchEditTipBtn" data-act="edit" aria-label="Edit color">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M12 20h9"/>
<path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4 12.5-12.5z"/>
</svg>
</button>
<button type="button" class="wmeRcSwatchEditTipBtn" data-act="del" aria-label="Remove color">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M3 6h18"/>
<path d="M8 6V4h8v2"/>
<path d="M19 6l-1 14H6L5 6"/>
<path d="M10 11v6"/>
<path d="M14 11v6"/>
</svg>
</button>
`;
swatchEditTipEl.addEventListener("pointerenter", () => {
if (swatchEditTipHideT){ clearTimeout(swatchEditTipHideT); swatchEditTipHideT = null; }
});
swatchEditTipEl.addEventListener("pointerleave", () => hideSwatchEditTip());
swatchEditTipEl.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
const btn = e.target && e.target.closest ? e.target.closest(".wmeRcSwatchEditTipBtn") : null;
const act = btn && btn.dataset ? btn.dataset.act : null;
if (!act) return;
if (!swatchEditAnchor || !Number.isFinite(swatchEditIndex)) return;
const arr = loadCustomPinColors();
if (act === "edit"){
const c = arr[swatchEditIndex];
if (!c) return;
openPop({ initial: c, editIndex: swatchEditIndex, anchorEl: swatchEditAnchor });
return;
}
if (act === "del"){
if (swatchEditIndex < 0 || swatchEditIndex >= arr.length) return;
arr.splice(swatchEditIndex, 1);
saveCustomPinColors(arr);
customColors = loadCustomPinColors();
renderCustomSwatches();
hideSwatchEditTip(true);
}
});
document.body.appendChild(swatchEditTipEl);
}
function showSwatchEditTip(anchorEl, idx){
ensureSwatchEditTip();
swatchEditAnchor = anchorEl;
swatchEditIndex = idx;
if (swatchEditTipHideT){ clearTimeout(swatchEditTipHideT); swatchEditTipHideT = null; }
const r = anchorEl.getBoundingClientRect();
const vw = window.innerWidth || document.documentElement.clientWidth || 9999;
swatchEditTipEl.style.display = "inline-flex";
swatchEditTipEl.style.visibility = "hidden";
swatchEditTipEl.style.left = "0px";
swatchEditTipEl.style.top = "0px";
const tipW = Math.max(1, swatchEditTipEl.getBoundingClientRect().width || swatchEditTipEl.offsetWidth || 1);
let left = r.left + (r.width / 2) - (tipW / 2);
left = Math.max(8, Math.min(vw - tipW - 8, left));
const top = r.bottom + 6;
swatchEditTipEl.style.left = left + "px";
swatchEditTipEl.style.top = top + "px";
swatchEditTipEl.style.visibility = "visible";
}
function hideSwatchEditTip(immediate){
if (!swatchEditTipEl) return;
const doHide = () => { try{ swatchEditTipEl.style.display = "none"; }catch{} };
if (immediate) return doHide();
if (swatchEditTipHideT) clearTimeout(swatchEditTipHideT);
swatchEditTipHideT = setTimeout(doHide, 220);
}
function addSwatch(c, { custom=false, index=null } = {}){
const sw = document.createElement("div");
sw.className = "wmeRcColorSwatch" + (custom ? " custom" : "");
sw.style.setProperty("--c", c);
sw.dataset.c = normalizePinColor(c);
if (custom) sw.dataset.customIndex = String(index);
sw.title = c;
sw.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
setColor(c);
if (custom) {
editCustomIndex = index;
hideSwatchEditTip(true);
}
});
if (custom) {
sw.addEventListener("pointerenter", () => showSwatchEditTip(sw, index));
sw.addEventListener("pointerleave", () => hideSwatchEditTip());
}
swatches.push(sw);
colorRow.appendChild(sw);
}
for (const c of PIN_COLOR_PRESETS) addSwatch(c);
function renderCustomSwatches(){
try{
const oldSep = colorRow.querySelector(".wmeRcColorSep");
if (oldSep) oldSep.remove();
}catch{}
for (let i = swatches.length - 1; i >= 0; i--){
const s = swatches[i];
if (s && s.dataset && s.dataset.customIndex != null) {
try { s.remove(); } catch {}
swatches.splice(i, 1);
}
}
const afterEl = colorRow.querySelectorAll(".wmeRcColorSwatch").item(PIN_COLOR_PRESETS.length - 1);
if (customColors && customColors.length){
const sep = document.createElement("div");
sep.className = "wmeRcColorSep";
sep.textContent = "|";
colorRow.insertBefore(sep, plusBtn);
}
for (let i = 0; i < customColors.length; i++){
const c = customColors[i];
const sw = document.createElement("div");
sw.className = "wmeRcColorSwatch custom";
sw.style.setProperty("--c", c);
sw.dataset.c = normalizePinColor(c);
sw.dataset.customIndex = String(i);
sw.title = c;
sw.addEventListener("click", (e) => { e.preventDefault(); e.stopPropagation(); setColor(c); });
sw.addEventListener("pointerenter", () => showSwatchEditTip(sw, i));
sw.addEventListener("pointerleave", () => hideSwatchEditTip());
colorRow.insertBefore(sw, plusBtn);
swatches.push(sw);
}
for (const s of swatches) s.classList.toggle("sel", s.dataset.c === chosenColor);
}
const plusBtn = document.createElement("div");
plusBtn.className = "wmeRcColorPlus";
plusBtn.textContent = "+";
plusBtn.title = "Custom color";
plusBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
editCustomIndex = null;
openCustomColorPop({ initial: chosenColor, editIndex: null });
});
colorRow.appendChild(plusBtn);
let colorPop = null;
function parseAnyColor(s){
try{
const str = String(s || "").trim();
if (!str) return null;
if (str.startsWith("#")) return normalizePinColor(str);
const m = str.match(/^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})(?:\s*,\s*([0-9.]+)\s*)?\)$/i);
if (m){
const r = Math.max(0, Math.min(255, Number(m[1])));
const g = Math.max(0, Math.min(255, Number(m[2])));
const b = Math.max(0, Math.min(255, Number(m[3])));
return normalizePinColor(`#${((1<<24)+(r<<16)+(g<<8)+b).toString(16).slice(1)}`);
}
return null;
}catch{ return null; }
}
function hexToRgb(hex){
try{
const h = normalizePinColor(hex);
const x = h.replace("#","");
const r = parseInt(x.slice(0,2),16);
const g = parseInt(x.slice(2,4),16);
const b = parseInt(x.slice(4,6),16);
return { r,g,b };
}catch{ return { r:0,g:0,b:0 }; }
}
function positionColorPop(anchorEl){
try{
const r = anchorEl.getBoundingClientRect();
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
const w = 286;
let left = r.right + 10;
if (left + w > vw - 10) left = r.left - w - 10;
left = Math.min(vw - w - 10, Math.max(10, left));
let top = Math.min(vh - 10 - 220, r.bottom + 8);
if (top < 10) top = 10;
colorPop.style.left = left + "px";
colorPop.style.top = top + "px";
}catch{}
}
function closeColorPop(){
try { if (colorPop) colorPop.remove(); } catch {}
colorPop = null;
try { document.removeEventListener("pointerdown", onPopOutside, true); } catch {}
}
function onPopOutside(e){
try{
if (!colorPop) return;
if (colorPop.contains(e.target)) return;
if (plusBtn.contains(e.target)) return;
closeColorPop();
}catch{}
}
function openCustomColorPop({ initial, editIndex, anchorEl }){
closeColorPop();
colorPop = document.createElement("div");
colorPop.className = "wmeRcColorPop wmeRcColorPopAdv";
colorPop.tabIndex = -1;
let cur = normalizePinColor(initial || chosenColor);
const isEditingPreset = Number.isFinite(editIndex) && editIndex != null;
const clamp01 = (v) => Math.max(0, Math.min(1, v));
const clamp255 = (v) => Math.max(0, Math.min(255, v|0));
const hexToRgb = (hex) => {
const h = String(hex||"").trim().replace(/^#/,"");
if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
return { r: parseInt(h.slice(0,2),16), g: parseInt(h.slice(2,4),16), b: parseInt(h.slice(4,6),16) };
};
const rgbToHex = (r,g,b) => "#" + [r,g,b].map(v => clamp255(v).toString(16).padStart(2,"0")).join("").toUpperCase();
const rgbToHsv = (r,g,b) => {
r/=255; g/=255; b/=255;
const max = Math.max(r,g,b), min = Math.min(r,g,b);
const d = max - min;
let h = 0;
if (d !== 0){
if (max === r) h = ((g-b)/d) % 6;
else if (max === g) h = (b-r)/d + 2;
else h = (r-g)/d + 4;
h *= 60;
if (h < 0) h += 360;
}
const s = max === 0 ? 0 : d / max;
const v = max;
return { h, s, v };
};
const hsvToRgb = (h,s,v) => {
h = ((h%360)+360)%360;
s = clamp01(s); v = clamp01(v);
const c = v * s;
const x = c * (1 - Math.abs(((h/60)%2) - 1));
const m = v - c;
let rp=0,gp=0,bp=0;
if (h < 60){ rp=c; gp=x; bp=0; }
else if (h < 120){ rp=x; gp=c; bp=0; }
else if (h < 180){ rp=0; gp=c; bp=x; }
else if (h < 240){ rp=0; gp=x; bp=c; }
else if (h < 300){ rp=x; gp=0; bp=c; }
else { rp=c; gp=0; bp=x; }
return { r: Math.round((rp+m)*255), g: Math.round((gp+m)*255), b: Math.round((bp+m)*255) };
};
const rgbToHsl = (r,g,b) => {
r/=255; g/=255; b/=255;
const max = Math.max(r,g,b), min = Math.min(r,g,b);
let h=0,s=0;
const l = (max+min)/2;
const d = max-min;
if (d !== 0){
s = d / (1 - Math.abs(2*l - 1));
if (max === r) h = ((g-b)/d) % 6;
else if (max === g) h = (b-r)/d + 2;
else h = (r-g)/d + 4;
h *= 60; if (h < 0) h += 360;
}
return { h, s, l };
};
const topRow = document.createElement("div");
topRow.className = "wmeRcColorTopRow";
const sw = document.createElement("div");
sw.className = "wmeRcColorSwatchBig";
sw.style.background = cur;
const stats = document.createElement('div');
stats.className = 'wmeRcColorStats';
const stat1 = document.createElement('div');
stat1.className = 'wmeRcColorStatLine';
stat1.innerHTML = `<span><span class="wmeRcColorStatKey">HEX</span><span class="wmeRcColorStatVal" data-k="hex">#000000</span></span>
<span><span class="wmeRcColorStatKey">RGB</span><span class="wmeRcColorStatVal" data-k="rgb">0, 0, 0</span></span>`;
const stat2 = document.createElement('div');
stat2.className = 'wmeRcColorStatLine';
stat2.innerHTML = `<span><span class="wmeRcColorStatKey">HSL</span><span class="wmeRcColorStatVal" data-k="hsl">0, 0%, 0%</span></span>`;
stats.appendChild(stat1);
stats.appendChild(stat2);
topRow.appendChild(sw);
topRow.appendChild(stats);
const pickerRow = document.createElement("div");
pickerRow.className = "wmeRcPickerRow";
const sv = document.createElement("div");
sv.className = "wmeRcPickerSV";
const svDot = document.createElement("div");
svDot.className = "wmeRcPickerSVDot";
sv.appendChild(svDot);
const hue = document.createElement("div");
hue.className = "wmeRcPickerHue";
const hueThumb = document.createElement("div");
hueThumb.className = "wmeRcPickerHueThumb";
hue.appendChild(hueThumb);
const fields = document.createElement("div");
fields.className = "wmeRcPickerFields";
const hexRow = document.createElement("div");
hexRow.className = "wmeRcPickerFieldRow";
const hexLbl = document.createElement("div");
hexLbl.className = "wmeRcPickerFieldLbl";
hexLbl.textContent = "HEX";
const hexInp = document.createElement("input");
hexInp.className = "wmeRcPickerField";
hexInp.inputMode = "text";
hexInp.autocomplete = "off";
hexInp.spellcheck = false;
hexInp.value = cur;
hexRow.appendChild(hexLbl);
hexRow.appendChild(hexInp);
const rRow = document.createElement("div");
rRow.className = "wmeRcPickerFieldRow";
const rLbl = document.createElement("div");
rLbl.className = "wmeRcPickerFieldLbl";
rLbl.textContent = "R";
const rInp = document.createElement("input");
rInp.className = "wmeRcPickerField";
rInp.inputMode = "numeric";
rInp.autocomplete = "off";
rInp.spellcheck = false;
rRow.appendChild(rLbl);
rRow.appendChild(rInp);
const gRow = document.createElement("div");
gRow.className = "wmeRcPickerFieldRow";
const gLbl = document.createElement("div");
gLbl.className = "wmeRcPickerFieldLbl";
gLbl.textContent = "G";
const gInp = document.createElement("input");
gInp.className = "wmeRcPickerField";
gInp.inputMode = "numeric";
gInp.autocomplete = "off";
gInp.spellcheck = false;
gRow.appendChild(gLbl);
gRow.appendChild(gInp);
const bRow = document.createElement("div");
bRow.className = "wmeRcPickerFieldRow";
const bLbl = document.createElement("div");
bLbl.className = "wmeRcPickerFieldLbl";
bLbl.textContent = "B";
const bInp = document.createElement("input");
bInp.className = "wmeRcPickerField";
bInp.inputMode = "numeric";
bInp.autocomplete = "off";
bInp.spellcheck = false;
bRow.appendChild(bLbl);
bRow.appendChild(bInp);
fields.appendChild(hexRow);
fields.appendChild(rRow);
fields.appendChild(gRow);
fields.appendChild(bRow);
pickerRow.appendChild(sv);
pickerRow.appendChild(hue);
pickerRow.appendChild(fields);
const actions = document.createElement("div");
actions.className = "wmeRcColorActions";
const savePresetBtn = document.createElement("button");
savePresetBtn.type = "button";
savePresetBtn.className = "wmeRcColorSavePreset";
savePresetBtn.textContent = isEditingPreset ? "Save" : "Save preset";
const delBtn = document.createElement("button");
delBtn.type = "button";
delBtn.className = "wmeRcColorDelete";
delBtn.textContent = "Delete";
const doneBtn = document.createElement("button");
doneBtn.type = "button";
doneBtn.className = "wmeRcColorDone";
doneBtn.textContent = "Done";
if (isEditingPreset){
actions.appendChild(delBtn);
actions.appendChild(savePresetBtn);
} else {
actions.appendChild(savePresetBtn);
}
actions.appendChild(doneBtn);
colorPop.appendChild(topRow);
colorPop.appendChild(pickerRow);
colorPop.appendChild(actions);
let hsv;
const seedRgb = hexToRgb(cur) || { r: 0, g: 122, b: 255 };
hsv = rgbToHsv(seedRgb.r, seedRgb.g, seedRgb.b);
function renderSV(){
const base = hsvToRgb(hsv.h, 1, 1);
sv.style.background = `linear-gradient(to top, rgba(0,0,0,1), rgba(0,0,0,0)), linear-gradient(to right, rgba(255,255,255,1), rgba(255,255,255,0)), rgb(${base.r},${base.g},${base.b})`;
svDot.style.left = (hsv.s * 100) + "%";
svDot.style.top = ((1 - hsv.v) * 100) + "%";
hueThumb.style.top = (clamp01(hsv.h / 360) * 100) + "%";
}
function renderStats(){
const rgb = hsvToRgb(hsv.h, hsv.s, hsv.v);
const hex = rgbToHex(rgb.r, rgb.g, rgb.b);
cur = normalizePinColor(hex);
sw.style.background = cur;
hexInp.value = cur;
rInp.value = String(rgb.r);
gInp.value = String(rgb.g);
bInp.value = String(rgb.b);
try{
sv.style.boxShadow = `inset 0 1px 0 rgba(255,255,255,.06), 0 18px 34px rgba(0,0,0,.28)`;
hue.style.boxShadow = `inset 0 1px 0 rgba(255,255,255,.06), 0 18px 34px rgba(0,0,0,.28)`;
}catch{}
try{
const br = (rgb.r*299 + rgb.g*587 + rgb.b*114) / 1000;
doneBtn.style.background = cur;
doneBtn.style.color = (br > 160) ? 'rgba(0,0,0,.82)' : '#fff';
}catch{}
const hexEl = colorPop.querySelector('[data-k="hex"]');
const rgbEl = colorPop.querySelector('[data-k="rgb"]');
const hslEl = colorPop.querySelector('[data-k="hsl"]');
if (hexEl) hexEl.textContent = cur.toUpperCase();
if (rgbEl) rgbEl.textContent = `${rgb.r}, ${rgb.g}, ${rgb.b}`;
const hsl = rgbToHsl(rgb.r, rgb.g, rgb.b);
if (hslEl) hslEl.textContent = `${Math.round(hsl.h)}, ${Math.round(hsl.s*100)}%, ${Math.round(hsl.l*100)}%`;
}
function applyHex(hex){
const rgb = hexToRgb(hex);
if (!rgb) return;
hsv = rgbToHsv(rgb.r, rgb.g, rgb.b);
renderSV();
renderStats();
}
function applyRgb(r,g,b){
r = clamp255(r); g = clamp255(g); b = clamp255(b);
hsv = rgbToHsv(r,g,b);
renderSV();
renderStats();
}
function pointToSV(e){
const r = sv.getBoundingClientRect();
const x = clamp01((e.clientX - r.left) / Math.max(1, r.width));
const y = clamp01((e.clientY - r.top) / Math.max(1, r.height));
hsv.s = x;
hsv.v = 1 - y;
renderSV();
renderStats();
}
function pointToHue(e){
const r = hue.getBoundingClientRect();
const y = clamp01((e.clientY - r.top) / Math.max(1, r.height));
hsv.h = y * 360;
renderSV();
renderStats();
}
function bindDrag(el, onMove){
const onDown = (e) => {
e.preventDefault();
try { el.setPointerCapture(e.pointerId); } catch {}
onMove(e);
const move = (ev) => onMove(ev);
const up = () => {
document.removeEventListener("pointermove", move, true);
document.removeEventListener("pointerup", up, true);
};
document.addEventListener("pointermove", move, true);
document.addEventListener("pointerup", up, true);
};
el.addEventListener("pointerdown", onDown);
}
bindDrag(sv, pointToSV);
bindDrag(hue, pointToHue);
hexInp.addEventListener("input", () => {
const v = hexInp.value.trim();
if (/^#?[0-9a-fA-F]{6}$/.test(v)) applyHex(v.startsWith("#")?v:("#"+v));
});
const rgbInputHandler = () => {
const rv = parseInt(rInp.value, 10);
const gv = parseInt(gInp.value, 10);
const bv = parseInt(bInp.value, 10);
if (!Number.isFinite(rv) || !Number.isFinite(gv) || !Number.isFinite(bv)) return;
applyRgb(rv, gv, bv);
};
rInp.addEventListener("input", rgbInputHandler);
gInp.addEventListener("input", rgbInputHandler);
bInp.addEventListener("input", rgbInputHandler);
savePresetBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
const norm = normalizePinColor(cur);
let arr = loadCustomPinColors();
const already = arr.findIndex(c => normalizePinColor(c) === norm);
if (already !== -1 && !(Number.isFinite(editIndex) && editIndex != null && already === editIndex)) {
try { toast("That preset already exists."); } catch {}
return;
}
if (isEditingPreset){
if (editIndex >= 0 && editIndex < arr.length){
arr[editIndex] = norm;
saveCustomPinColors(arr);
customColors = loadCustomPinColors();
renderCustomSwatches();
}
setColor(norm);
closeColorPop();
return;
}
if (arr.length >= 4){
arr[arr.length - 1] = norm;
} else {
arr.push(norm);
}
saveCustomPinColors(arr);
customColors = loadCustomPinColors();
renderCustomSwatches();
setColor(norm);
closeColorPop();
});
delBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
if (!isEditingPreset) return;
let arr = loadCustomPinColors();
if (editIndex >= 0 && editIndex < arr.length){
arr.splice(editIndex, 1);
saveCustomPinColors(arr);
customColors = loadCustomPinColors();
renderCustomSwatches();
}
closeColorPop();
});
doneBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
setColor(cur);
closeColorPop();
});
renderSV();
renderStats();
document.body.appendChild(colorPop);
positionColorPop(anchorEl || plusBtn);
document.addEventListener("pointerdown", onPopOutside, true);
syncMeta();
}
renderCustomSwatches();
setColor(chosenColor);
const groups = loadPinGroups();
let chosenGroupId = getLastGroup();
const groupLbl = document.createElement("div");
groupLbl.className = "wmeRcHint";
groupLbl.textContent = "Folder";
const groupPick = document.createElement("div");
groupPick.className = "wmeRcSoundPick";
groupPick.tabIndex = 0;
const groupBtn = document.createElement("div");
groupBtn.className = "wmeRcSoundBtn";
const groupBtnLabel = document.createElement("div");
groupBtnLabel.className = "wmeRcSoundBtnLabel";
const groupCaret = document.createElement("div");
groupCaret.className = "wmeRcSoundCaret";
groupCaret.innerHTML = ICONS.chevDown;
groupBtn.appendChild(groupBtnLabel);
groupBtn.appendChild(groupCaret);
groupPick.appendChild(groupBtn);
const groupMenu = document.createElement("div");
groupMenu.className = "wmeRcSoundMenu wmeRcSoundMenuPortal";
groupMenu.style.display = "none";
try { document.body.appendChild(groupMenu); } catch {}
const getGroupLabel = (id) => {
try {
const gg = loadPinGroups().find(x => x && x.id === id);
return gg ? (gg.name || "(no folder)") : "(no folder)";
} catch { return "(no folder)"; }
};
const positionGroupMenu = () => {
try {
const r = groupBtn.getBoundingClientRect();
const vw = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
const vh = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
const w = Math.max(260, Math.min(r.width || 320, vw - 16));
let left = Math.max(8, Math.min(r.left, vw - w - 8));
let top = (r.bottom || 0) + 6;
const maxH = 240;
if (top + maxH + 8 > vh) top = Math.max(8, (r.top || 0) - 6 - maxH);
groupMenu.style.left = left + "px";
groupMenu.style.top = top + "px";
groupMenu.style.width = w + "px";
const avail = Math.max(120, vh - top - 10);
groupMenu.style.maxHeight = Math.min(maxH, avail) + "px";
} catch {}
};
const rebuildGroupMenu = () => {
groupMenu.innerHTML = "";
const gs = loadPinGroups();
for (const g of gs) {
const item = document.createElement("div");
item.className = "wmeRcSoundItem";
item.textContent = g.name;
item.dataset.groupId = g.id;
item.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
chosenGroupId = normalizeGroupId(g.id);
groupBtnLabel.textContent = getGroupLabel(chosenGroupId);
try { setLastGroup(chosenGroupId); } catch {}
toggleGroupMenu(false);
});
groupMenu.appendChild(item);
}
const newItem = document.createElement("div");
newItem.className = "wmeRcSoundItem";
newItem.textContent = "+ New Folder";
newItem.dataset.groupId = "__new__";
newItem.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
toggleGroupMenu(false);
openGroupNameModal({
title: "New folder",
placeholder: "Name",
okText: "Create",
onCancel: () => {},
onSubmit: (nm, emoji) => {
const id = createPinGroup(nm, emoji);
chosenGroupId = id;
try { setLastGroup(chosenGroupId); } catch {}
groupBtnLabel.textContent = getGroupLabel(chosenGroupId);
rebuildGroupMenu();
}
});
});
groupMenu.appendChild(newItem);
};
const toggleGroupMenu = (open) => {
const isOpen = groupPick.getAttribute("data-open") === "1";
const next = (typeof open === "boolean") ? open : !isOpen;
groupPick.setAttribute("data-open", next ? "1" : "0");
try {
if (next) {
positionGroupMenu();
groupMenu.style.display = "block";
} else {
groupMenu.style.display = "none";
}
} catch {}
};
groupBtn.addEventListener("click", (e) => {
e.preventDefault(); e.stopPropagation();
toggleGroupMenu();
});
const onDocDown = (e) => {
try {
if (!groupPick.contains(e.target) && !groupMenu.contains(e.target)) {
groupPick.setAttribute("data-open", "0");
groupMenu.style.display = "none";
}
} catch {}
};
document.addEventListener("pointerdown", onDocDown, true);
try { window.addEventListener("resize", positionGroupMenu, true); } catch {}
try { window.addEventListener("scroll", positionGroupMenu, true); } catch {}
const cleanupGroupPick = () => {
try { document.removeEventListener("pointerdown", onDocDown, true); } catch {}
try { window.removeEventListener("resize", positionGroupMenu, true); } catch {}
try { window.removeEventListener("scroll", positionGroupMenu, true); } catch {}
try { groupMenu.remove(); } catch {}
};
try {
const mo = new MutationObserver(() => {
try {
if (!document.body.contains(modal)) {
cleanupGroupPick();
mo.disconnect();
}
} catch {}
});
mo.observe(document.body, { childList: true });
} catch {}
rebuildGroupMenu();
groupBtnLabel.textContent = getGroupLabel(chosenGroupId);
setColor(chosenColor);
const remNowWrap = document.createElement("div");
remNowWrap.className = "wmeRcHint";
remNowWrap.innerHTML = `
<label class="wmeRcInlineToggle">
<input type="checkbox" class="wmeRcRemNowChk">
<span class="wmeRcSwitchTrack"><span class="wmeRcSwitchThumb"></span></span>
<span class="wmeRcSwitchLabel">Set a reminder</span>
</label>
`;
const remNowChk = remNowWrap.querySelector("input");
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const btnCancel = document.createElement("div");
btnCancel.className = "wmeRcModalBtn";
btnCancel.textContent = "Cancel";
btnCancel.addEventListener("click", () => { try { if (typeof cleanupSoundPick === "function") cleanupSoundPick(); } catch {} close(); });
const btnPin = document.createElement("div");
btnPin.className = "wmeRcModalBtn primary";
btnPin.textContent = "Pin";
btnPin.addEventListener("click", async () => {
const v = validatePinName(nameInp.value, fallbackName);
if (!v.ok) { toast(v.msg); try { nameInp.focus(); nameInp.select(); } catch {} return; }
const name = v.value;
const reminderAt = null;
const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
const newPin = { id, name, color: chosenColor, groupId: chosenGroupId, lon: Number(ll.lon), lat: Number(ll.lat), zoom: Number.isFinite(zoom) ? zoom : null, createdAt: Date.now(), reminderAt, reminderDone: false, reminderType: null, reminderUnit: null, reminderValue: null , reminderNote: "" };
const cur = loadPins();
cur.push(newPin);
savePins(cur);
try { setLastGroup(chosenGroupId); } catch {}
renderPinsPanel();
startReminderLoop();
toast("Pinned place saved");
closeAllMenus();
close();
if (remNowChk && remNowChk.checked) {
setTimeout(() => { try { openReminderModal(id); } catch {} }, 60);
}
});
actions.appendChild(btnCancel);
actions.appendChild(btnPin);
const nameLimitMsg = attachMaxLen(nameInp, 32);
body.appendChild(nameLimitMsg ? nameLimitMsg.wrap : nameInp);
if (nameLimitMsg && nameLimitMsg.msg) body.appendChild(nameLimitMsg.msg);
body.appendChild(colorRow);
body.appendChild(groupLbl);
body.appendChild(groupPick);
body.appendChild(remNowWrap);
body.appendChild(actions);
setTimeout(() => { try { nameInp.focus(); nameInp.select(); } catch {} }, 60);
},
});
}
function clearSubCloseTimer() {
if (menuState.subCloseTimer) {
clearTimeout(menuState.subCloseTimer);
menuState.subCloseTimer = null;
}
}
function closeSubMenu2() {
try { if (menuState.sub2AnchorRow) menuState.sub2AnchorRow.classList.remove("submenuOpen"); } catch {}
menuState.sub2AnchorRow = null;
if (menuState.sub2) menuState.sub2.remove();
menuState.sub2 = null;
menuState.sub2Context = null;
}
function closeSubMenu() {
closeSubMenu2();
try { if (menuState.subAnchorRow) menuState.subAnchorRow.classList.remove("submenuOpen"); } catch {}
menuState.subAnchorRow = null;
if (menuState.sub) menuState.sub.remove();
menuState.sub = null;
menuState.subContext = null;
}
function detachOutsideHandlers() {
if (menuState.outsideCloseHandler) {
document.removeEventListener("mousedown", menuState.outsideCloseHandler, true);
document.removeEventListener("touchstart", menuState.outsideCloseHandler, true);
document.removeEventListener("contextmenu", menuState.outsideCloseHandler, true);
menuState.outsideCloseHandler = null;
}
if (menuState.escHandler) {
document.removeEventListener("keydown", menuState.escHandler, true);
menuState.escHandler = null;
}
}
function closeAllMenus() {
clearSubCloseTimer();
closeSubMenu();
if (menuState.root) menuState.root.remove();
menuState.sub2 = null;
menuState.sub = null;
menuState.root = null;
menuState.sub2Context = null;
menuState.subContext = null;
menuState.selectionSnapshot = null;
detachOutsideHandlers();
}
function scheduleCloseSub(delay = 160, mode = "auto") {
clearSubCloseTimer();
menuState.subCloseTimer = setTimeout(() => {
if (mode === "sub2") {
closeSubMenu2();
return;
}
if (mode === "all") {
closeSubMenu2();
closeSubMenu();
return;
}
if (menuState.sub2) closeSubMenu2();
else closeSubMenu();
}, delay);
}
function positionMenu(menu, x, y) {
(document.body || document.documentElement).appendChild(menu);
const r = menu.getBoundingClientRect();
const pad = 8;
let px = x, py = y;
if (px + r.width + pad > innerWidth) px = Math.max(pad, innerWidth - r.width - pad);
if (py + r.height + pad > innerHeight) py = Math.max(pad, innerHeight - r.height - pad);
menu.style.left = `${px}px`;
menu.style.top = `${py}px`;
}
function updateRootRowSub(kind, newSubText) {
try {
const root = menuState.root;
if (!root) return;
const row = root.querySelector(`.wmeRcItem[data-kind="${CSS.escape(kind)}"]`);
if (!row) return;
const sub = row.querySelector(".wmeRcSubText");
if (!sub) return;
sub.textContent = String(newSubText ?? "");
} catch {}
}
function refreshActiveSubmenuIf(kind) {
try {
const ctx = menuState.subContext;
if (!ctx || ctx.kind !== kind || !menuState.sub) return;
const items = ctx.getItems ? ctx.getItems() : [];
openSubMenuForRow(ctx.anchorEl, items, { kind: ctx.kind, getItems: ctx.getItems, keepContext: true });
} catch (e) {
console.error(e);
}
}
function buildSpeedGrid(spec) {
const wrap = document.createElement("div");
wrap.className = "wmeRcSpeedWrap";
const title = document.createElement("div");
title.className = "wmeRcSpeedTitle";
const left = document.createElement("div");
left.style.display = "flex";
left.style.gap = "8px";
left.style.alignItems = "center";
left.innerHTML = `<span class="wmeRcMuted">${spec.titleLeft || "Speed"}</span>`;
const right = document.createElement("div");
right.className = "wmeRcMuted";
right.textContent = spec.titleRight || "";
title.appendChild(left);
title.appendChild(right);
wrap.appendChild(title);
if (spec.chipsEl) {
const chipsWrap = document.createElement("div");
chipsWrap.style.margin = "8px 0 8px 0";
chipsWrap.appendChild(spec.chipsEl);
wrap.appendChild(chipsWrap);
}
const grid = document.createElement("div");
grid.className = "wmeRcSpeedGrid";
for (const btn of (spec.buttons || [])) {
const b = document.createElement("div");
b.className = "wmeRcSpeedBtn" + (btn.selected ? " sel" : "");
b.title = btn.title || "";
if (btn.html) b.innerHTML = btn.html;
else b.textContent = btn.text;
b.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
try { await btn.onClick(); }
catch (err) { console.error(err); toast(String(err?.message || err)); }
});
grid.appendChild(b);
}
wrap.appendChild(grid);
return wrap;
}
function buildMenuElement(spec) {
ensureCSS();
const menu = document.createElement("div");
menu.className = "wmeRcMenu" + (spec.isSub ? " sub" : "");
menu.setAttribute("role", "menu");
menu.addEventListener("mousedown", (e) => { e.stopPropagation(); }, false);
menu.addEventListener("click", (e) => { e.stopPropagation(); }, false);
menu.addEventListener("contextmenu", (e) => { e.preventDefault(); e.stopPropagation(); }, true);
if (spec.headerLeft) {
const hdr = document.createElement("div");
hdr.className = "wmeRcHdr";
hdr.innerHTML = `
<div class="wmeRcHdrLeft">
<div class="wmeRcHdrTitle">${spec.headerLeft}</div>
</div>
<div class="wmeRcMuted">${spec.headerRight}</div>
`;
menu.appendChild(hdr);
}
for (const it of spec.items) {
if (it.type === "speedGrid") { menu.appendChild(buildSpeedGrid(it)); continue; }
if (it.type === "sep") { const sep = document.createElement("div"); sep.className = "wmeRcSep"; menu.appendChild(sep); continue; }
const row = document.createElement("div");
row.className = "wmeRcItem" + (it.disabled ? " disabled" : "") + (it.selected ? " selected" : "");
if (it.kind === "section") row.className += " sectionHdr";
row.setAttribute("role", "menuitem");
if (it.kind) row.dataset.kind = String(it.kind);
const left = document.createElement("div");
left.className = "wmeRcLeft";
left.innerHTML = `<div>${it.label}</div>`;
const right = document.createElement("div");
right.style.display = "flex";
right.style.gap = "8px";
right.style.alignItems = "center";
if (it.iconButton && typeof it.iconButton.onClick === "function") {
const b = document.createElement("div");
b.className = "wmeRcMiniBtn wmeRcMiniIcon";
b.title = it.iconButton.title || "";
b.innerHTML = it.iconButton.html || it.iconButton.label || "⚙";
b.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
try { await it.iconButton.onClick(); }
catch (err) { console.error(err); toast(String(err?.message || err)); }
});
right.appendChild(b);
}
if (it.rightButton && typeof it.rightButton.onClick === "function") {
const b = document.createElement("div");
b.className = "wmeRcMiniBtn";
b.innerHTML = it.rightButton.html || it.rightButton.label || "Open";
b.addEventListener("click", async (e) => {
e.preventDefault();
e.stopPropagation();
try { await it.rightButton.onClick(); }
catch (err) { console.error(err); toast(String(err?.message || err)); }
});
right.appendChild(b);
} else if (it.check) {
right.insertAdjacentHTML("beforeend", `<span class="wmeRcCheck">✓</span>`);
} else if (it.kbd) {
right.insertAdjacentHTML("beforeend", `<span class="wmeRcKbd">${it.kbd}</span>`);
} else if (it.submenu) {
right.insertAdjacentHTML("beforeend", `<span class="wmeRcChevron">›</span>`);
}
row.appendChild(left);
row.appendChild(right);
if (!it.disabled && typeof it.onClick === "function" && !it.submenu) {
row.addEventListener("click", async (e) => {
e.preventDefault(); e.stopPropagation();
try { await it.onClick(); }
catch (err) { console.error(err); toast(String(err?.message || err)); }
});
}
if (!it.disabled && it.submenu && typeof it.getSubmenuItems === "function") {
row.addEventListener("mouseenter", () => {
if (menuState.subCloseTimer) { clearTimeout(menuState.subCloseTimer); menuState.subCloseTimer = null; }
let subItems = [];
try { subItems = it.getSubmenuItems() || []; } catch (e) { console.error(e); }
openSubMenuForRow(row, subItems, { kind: it.submenuKind || "generic", getItems: it.getSubmenuItems, headerLeft: it.submenuHeaderLeft || "", headerRight: it.submenuHeaderRight || "" });
});
row.addEventListener("mouseleave", () => scheduleCloseSub(180));
}
menu.appendChild(row);
}
if (spec.headerLeft) {
menu.addEventListener("mouseleave", () => scheduleCloseSub(180, "all"));
menu.addEventListener("mouseenter", () => {
if (menuState.subCloseTimer) { clearTimeout(menuState.subCloseTimer); menuState.subCloseTimer = null; }
});
}
return menu;
}
function attachOutsideCloseHandlers() {
const handler = (e) => {
const root = menuState.root;
const sub = menuState.sub;
const sub2 = menuState.sub2;
const t = e.target;
if (root && (root === t || root.contains(t))) return;
if (sub && (sub === t || sub.contains(t))) return;
if (sub2 && (sub2 === t || sub2.contains(t))) return;
const snap = menuState.selectionSnapshot;
let cx = e.clientX, cy = e.clientY;
if (e.touches && e.touches[0]) { cx = e.touches[0].clientX; cy = e.touches[0].clientY; }
const isLeftMouse = (e.type === "mousedown") && (e.button === 0);
const isLeftTouch = (e.type === "touchstart");
const isLeft = isLeftMouse || isLeftTouch;
const mapClick = isLeft && Number.isFinite(cx) && Number.isFinite(cy) && isMapClick(cx, cy);
const hasSnap = Array.isArray(snap) && snap.length;
const noMods = !(e.shiftKey || e.ctrlKey || e.metaKey || e.altKey);
const shouldKeepSelection = mapClick && hasSnap && noMods;
if (shouldKeepSelection) {
try { e.preventDefault(); } catch {}
try { e.stopImmediatePropagation(); } catch {}
try { e.stopPropagation(); } catch {}
}
closeAllMenus();
if (shouldKeepSelection) {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
try { setSelectionToSegmentIdsSilent(snap); } catch {}
});
});
}
};
menuState.outsideCloseHandler = handler;
document.addEventListener("mousedown", handler, true);
document.addEventListener("contextmenu", handler, true);
document.addEventListener("touchstart", handler, { capture: true, passive: false });
}
function openRootMenu(x, y, headerLeft, headerRight, items) {
closeAllMenus();
menuState.selectionSnapshot = selectedSegmentIds();
const root = buildMenuElement({ headerLeft, headerRight, items, isSub: false });
menuState.root = root;
positionMenu(root, x, y);
attachOutsideCloseHandlers();
}
function openSubMenuForRow(rowEl, items, opts = {}) {
const keepCtx = !!opts.keepContext;
const prevCtx1 = menuState.subContext;
const prevCtx2 = menuState.sub2Context;
const parentMenu = rowEl?.closest?.('.wmeRcMenu');
const fromRoot = !!(parentMenu && menuState.root && parentMenu === menuState.root);
if (fromRoot) {
closeSubMenu();
} else {
closeSubMenu2();
}
if (!items || !items.length) return;
try { rowEl.classList.add("submenuOpen"); } catch {}
const sub = buildMenuElement({ headerLeft: opts.headerLeft || "", headerRight: opts.headerRight || "", items, isSub: true });
if (fromRoot) menuState.subAnchorRow = rowEl;
else menuState.sub2AnchorRow = rowEl;
if (fromRoot) menuState.sub = sub;
else menuState.sub2 = sub;
const rr = rowEl.getBoundingClientRect();
const margin = 8;
let x = rr.right + margin;
let y = rr.top - 8;
sub.style.left = "0px";
sub.style.top = "0px";
(document.body || document.documentElement).appendChild(sub);
const sr = sub.getBoundingClientRect();
sub.remove();
if (x + sr.width + 8 > innerWidth) x = Math.max(8, rr.left - margin - sr.width);
if (y + sr.height + 8 > innerHeight) y = Math.max(8, innerHeight - sr.height - 8);
positionMenu(sub, x, y);
sub.addEventListener("mouseenter", () => {
clearSubCloseTimer();
});
sub.addEventListener("mouseleave", () => {
scheduleCloseSub(180, fromRoot ? "auto" : "sub2");
});
if (keepCtx) {
if (fromRoot && prevCtx1) menuState.subContext = prevCtx1;
else if (!fromRoot && prevCtx2) menuState.sub2Context = prevCtx2;
} else {
const ctx = {
kind: String(opts.kind || "generic"),
anchorEl: rowEl,
getItems: typeof opts.getItems === "function" ? opts.getItems : () => items,
};
if (fromRoot) menuState.subContext = ctx;
else menuState.sub2Context = ctx;
}
}
function openModal(spec) {
ensureCSS();
closeAllMenus();
const backdrop = document.createElement("div");
backdrop.className = "wmeRcModalBackdrop";
const modal = document.createElement("div");
modal.className = "wmeRcModal";
const header = document.createElement("div");
header.className = "wmeRcModalHdr";
header.innerHTML = `
<div class="wmeRcModalTitle"><span class="wmeRcI">${spec.iconSvg || ICONS.gear}</span><span>${spec.title || ""}</span></div>
`;
const body = document.createElement("div");
body.className = "wmeRcModalBody";
modal.appendChild(header);
modal.appendChild(body);
let _onKey = null;
const close = () => {
try { if (_onKey) document.removeEventListener("keydown", _onKey, true); } catch {}
backdrop.classList.remove("show");
modal.classList.remove("show");
setTimeout(() => {
backdrop.remove();
modal.remove();
}, 180);
};
backdrop.addEventListener("click", (ev) => { try { ev.preventDefault(); ev.stopPropagation(); } catch {} close(); });
(document.body || document.documentElement).appendChild(backdrop);
(document.body || document.documentElement).appendChild(modal);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
backdrop.classList.add("show");
modal.classList.add("show");
});
});
if (typeof spec.bodyBuilder === "function") spec.bodyBuilder({ body, close, modal });
_onKey = (ev) => {
try {
if (!ev) return;
if (ev.key === "Escape") {
ev.preventDefault();
ev.stopPropagation();
close();
return;
}
if (ev.key !== "Enter") return;
if (ev.shiftKey || ev.altKey || ev.ctrlKey || ev.metaKey) return;
const t = ev.target;
const tag = t && t.tagName ? String(t.tagName).toLowerCase() : "";
if (tag === "textarea") return; // allow newlines
let targetBtn = modal.querySelector(".wmeRcModalBtn.primary");
if (!targetBtn) {
const btns = Array.from(modal.querySelectorAll(".wmeRcModalBtn"));
targetBtn = btns[btns.length - 1] || null;
}
if (!targetBtn) return;
ev.preventDefault();
ev.stopPropagation();
targetBtn.click();
} catch {}
};
try { document.addEventListener("keydown", _onKey, true); } catch {}
return { close };
}
function openChangelogModal() {
try { ensureCSS(); } catch {}
try {
localStorage.setItem(CHANGELOG_SEEN_KEY, SCRIPT_VERSION);
} catch {}
openModal({
iconSvg: ICONS.tools,
title: `What’s new — v${SCRIPT_VERSION}`,
bodyBuilder: ({ body, close }) => {
const wrap = document.createElement("div");
wrap.style.display = "flex";
wrap.style.flexDirection = "column";
wrap.style.gap = "10px";
const hint = document.createElement("div");
hint.className = "wmeRcHint";
hint.textContent = "Changes in this version:";
wrap.appendChild(hint);
const ul = document.createElement("ul");
ul.style.margin = "0 0 0 18px";
ul.style.padding = "0";
ul.style.display = "flex";
ul.style.flexDirection = "column";
ul.style.gap = "6px";
for (const it of (CHANGELOG_ITEMS || [])) {
const li = document.createElement("li");
li.textContent = String(it);
ul.appendChild(li);
}
wrap.appendChild(ul);
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const ok = document.createElement("div");
ok.className = "wmeRcModalBtn primary";
ok.textContent = "OK";
ok.addEventListener("click", () => close());
actions.appendChild(ok);
wrap.appendChild(actions);
body.appendChild(wrap);
},
});
}
function maybeShowChangelogOnce() {
let seen = null;
try { seen = localStorage.getItem(CHANGELOG_SEEN_KEY); } catch {}
if (String(seen || "") === String(SCRIPT_VERSION)) return;
openChangelogModal();
}
function openGroupNameModal(opts) {
try {
const title = (opts && opts.title) ? String(opts.title) : "New folder";
const initial = (opts && opts.initial != null) ? String(opts.initial) : "";
const okText = (opts && opts.okText) ? String(opts.okText) : "Create";
const cancelText = (opts && opts.cancelText) ? String(opts.cancelText) : "Cancel";
const placeholder = (opts && opts.placeholder) ? String(opts.placeholder) : "Name";
const onSubmit = (opts && typeof opts.onSubmit === "function") ? opts.onSubmit : null;
const onCancel = (opts && typeof opts.onCancel === "function") ? opts.onCancel : null;
openModal({
title,
iconSvg: ICONS.folder,
bodyBuilder: ({ body, close }) => {
const inp = document.createElement("input");
inp.className = "wmeRcInput";
inp.placeholder = placeholder;
inp.value = initial;
const groupNameLimitMsg = attachMaxLen(inp, 32);
const rowTop = document.createElement("div");
rowTop.className = "wmeRcRow wmeRcEmojiRow";
rowTop.style.alignItems = "stretch";
const emojiBtn = document.createElement("button");
emojiBtn.type = "button";
emojiBtn.className = "wmeRcEmojiBtn";
emojiBtn.title = "Pick emoji";
let selectedEmoji = (opts && typeof opts.initialEmoji === "string") ? String(opts.initialEmoji) : "";
emojiBtn.innerHTML = selectedEmoji ? selectedEmoji : "<span class='wmeRcNoEmojiIcon' aria-label='No emoji'>🙂</span>";
const emojiPop = document.createElement("div");
emojiPop.className = "wmeRcEmojiPop hidden";
const emojiGrid = document.createElement("div");
emojiGrid.className = "wmeRcEmojiGrid";
const EMOJIS = ["📍","📌","🗺️","🛣️","🛤️","🚧","⛔","🚫","🛑","⚠️","🚦","🚥","📷","🅿️","🚏","🚸","🌉","🌊","🏞️","✈️","🛫","🛬"];
const setEmoji = (em) => {
try { selectedEmoji = String(em || ""); } catch { selectedEmoji = ""; }
try { emojiBtn.innerHTML = selectedEmoji ? selectedEmoji : "<span class='wmeRcNoEmojiIcon' aria-label='No emoji'>🙂</span>"; } catch {}
try { emojiPop.classList.add("hidden"); } catch {}
};
try {
const bNone = document.createElement("button");
bNone.type = "button";
bNone.className = "wmeRcEmojiItem wmeRcEmojiItemNone";
bNone.innerHTML = "<span class='wmeRcNoEmojiIcon wmeRcNoEmojiIconSm' aria-label='No emoji'>🙂</span>";
bNone.addEventListener("click", (ev) => { try { ev.preventDefault(); ev.stopPropagation(); } catch {} setEmoji(""); });
emojiGrid.appendChild(bNone);
} catch {}
EMOJIS.forEach((em) => {
const b = document.createElement("button");
b.type = "button";
b.className = "wmeRcEmojiItem";
b.textContent = em;
b.addEventListener("click", (ev) => { try { ev.preventDefault(); ev.stopPropagation(); } catch {} setEmoji(em); });
emojiGrid.appendChild(b);
});
emojiPop.appendChild(emojiGrid);
emojiBtn.addEventListener("click", (ev) => {
try { ev.preventDefault(); ev.stopPropagation(); } catch {}
emojiPop.classList.toggle("hidden");
});
setTimeout(() => {
const closePop = (ev) => {
try {
if (!emojiPop.contains(ev.target) && ev.target !== emojiBtn) emojiPop.classList.add("hidden");
} catch {}
};
document.addEventListener("mousedown", closePop, true);
document.addEventListener("keydown", (ev) => { if (ev.key === "Escape") emojiPop.classList.add("hidden"); }, true);
}, 0);
rowTop.appendChild(emojiBtn);
rowTop.appendChild(groupNameLimitMsg ? groupNameLimitMsg.wrap : inp);
body.appendChild(rowTop);
if (groupNameLimitMsg && groupNameLimitMsg.msg) body.appendChild(groupNameLimitMsg.msg);
body.appendChild(emojiPop);
const hint = document.createElement("div");
hint.className = "wmeRcHint";
hint.textContent = "Emojis and symbols are allowed.";
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const btnCancel = document.createElement("div");
btnCancel.className = "wmeRcModalBtn";
btnCancel.textContent = cancelText;
btnCancel.addEventListener("click", () => {
try { onCancel && onCancel(); } catch {}
close();
});
const btnOk = document.createElement("div");
btnOk.className = "wmeRcModalBtn primary";
btnOk.textContent = okText;
const apply = () => {
const nm = String(inp.value || "").trim();
if (!nm) return;
try { onSubmit && onSubmit(nm, selectedEmoji); } catch {}
close();
};
btnOk.addEventListener("click", apply);
inp.addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.preventDefault(); apply(); }
if (e.key === "Escape") { e.preventDefault(); try { onCancel && onCancel(); } catch {}; close(); }
});
actions.appendChild(btnCancel);
actions.appendChild(btnOk);
body.appendChild(hint);
body.appendChild(actions);
setTimeout(() => { try { inp.focus(); inp.select(); } catch {} }, 50);
}
});
} catch(e) {
try { console.warn("[WME Pins] openGroupNameModal failed", e); } catch {}
try { opts && typeof opts.onCancel === "function" && opts.onCancel(); } catch {}
}
}
function openClearDefaultFolderPinsModal(opts) {
try {
const folderName = String(opts?.folderName || "(no folder)");
const count = Number(opts?.count || 0);
const onConfirm = (typeof opts?.onConfirm === "function") ? opts.onConfirm : null;
if (!count) return;
openModal({
title: "Clear pins",
iconSvg: ICONS.trash,
bodyBuilder: ({ body, close }) => {
const p = document.createElement("div");
p.className = "wmeRcHint";
p.style.opacity = ".92";
p.textContent = `Clear ${count} pin${count === 1 ? "" : "s"} from "${folderName}"?`;
const p2 = document.createElement("div");
p2.className = "wmeRcHint";
p2.style.opacity = ".75";
p2.style.marginTop = "6px";
p2.textContent = "This cannot be undone.";
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const btnCancel = document.createElement("div");
btnCancel.className = "wmeRcModalBtn";
btnCancel.textContent = "Cancel";
btnCancel.addEventListener("click", () => close());
const btnClear = document.createElement("div");
btnClear.className = "wmeRcModalBtn danger";
btnClear.textContent = "Clear";
btnClear.addEventListener("click", () => {
try { onConfirm && onConfirm(); } catch {}
close();
});
actions.appendChild(btnCancel);
actions.appendChild(btnClear);
body.appendChild(p);
body.appendChild(p2);
body.appendChild(actions);
}
});
} catch {}
}
function openRemoveFolderModal(gid) {
try {
gid = String(gid || "");
if (!gid || gid === "default") return;
const g = loadPinGroups().find(x => x && x.id === gid) || { name: "Folder" };
openModal({
title: "Remove folder",
iconSvg: ICONS.trash,
bodyBuilder: ({ body, close }) => {
const p = document.createElement("div");
p.className = "wmeRcHint";
p.style.opacity = ".92";
p.textContent = `What do you want to do with "${g.name || "Folder"}"?`;
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const btnCancel = document.createElement("div");
btnCancel.className = "wmeRcModalBtn";
btnCancel.textContent = "Cancel";
btnCancel.addEventListener("click", () => close());
const btnMove = document.createElement("div");
btnMove.className = "wmeRcModalBtn";
btnMove.textContent = "Delete folder";
btnMove.title = "Keeps pins and moves them to (no folder)";
btnMove.addEventListener("click", () => {
const ps = loadPins().map(pn => (normalizeGroupId(pn.groupId) === gid) ? ({ ...pn, groupId: "default" }) : pn);
savePins(ps);
const gs = loadPinGroups().filter(x => x && x.id !== gid);
savePinGroups(gs);
try { if (getCurrentGroupFilter() === gid) setCurrentGroupFilter("all"); } catch {}
close();
renderPinsPanel();
});
const btnDeleteAll = document.createElement("div");
btnDeleteAll.className = "wmeRcModalBtn danger";
btnDeleteAll.textContent = "Delete folder + pins";
btnDeleteAll.title = "Deletes folder and all pins inside";
btnDeleteAll.addEventListener("click", () => {
const ps = loadPins().filter(pn => normalizeGroupId(pn.groupId) !== gid);
savePins(ps);
const gs = loadPinGroups().filter(x => x && x.id !== gid);
savePinGroups(gs);
try { if (getCurrentGroupFilter() === gid) setCurrentGroupFilter("all"); } catch {}
close();
renderPinsPanel();
});
actions.appendChild(btnCancel);
actions.appendChild(btnMove);
actions.appendChild(btnDeleteAll);
body.appendChild(p);
body.appendChild(actions);
}
});
} catch {}
}
function isMapClick(clientX, clientY) {
try {
const mapEl = getMapContainerEl();
if (!mapEl) return false;
const r = mapEl.getBoundingClientRect();
if (clientX < r.left || clientX > r.right || clientY < r.top || clientY > r.bottom) return false;
// If the click is on UR panel/UI overlay, it's not a map click for our purposes.
const stack = (document.elementsFromPoint ? document.elementsFromPoint(clientX, clientY) : [document.elementFromPoint(clientX, clientY)]).filter(Boolean);
for (const el of stack.slice(0, 12)) {
if (!el || !el.closest) continue;
if (isInUpdateRequestPanel(el)) return false;
// Generic UI panels/cards/menus — let native WME handle these.
if (el.closest(".wmeRcMenu, .wmeRcModal, .wmeRcPinsPanel")) return false;
if (el.closest("wz-card, [role='dialog'], [role='menu'], .menu, .dropdown, .panel, .sidebar, .tooltip")) return false;
}
// A real map click typically hits the map canvas or something within the map container.
for (const el of stack) {
if (!el) continue;
if (el.tagName === "CANVAS") return true;
if (el.closest && el.closest("canvas")) return true;
if (el === mapEl || (el.closest && el.closest("#map, #WazeMap, .olMap, .wme-map"))) return true;
}
} catch {}
return false;
}
function isSegmentObjectType(v) {
if (typeof v === "string") return /segment/i.test(v);
const OT = sdk?.Editing?.ObjectType;
if (typeof v === "number" && OT && OT.SEGMENT === v) return true;
return /segment/i.test(String(v));
}
function extractSegmentIdsFromSelection(sel) {
if (Array.isArray(sel)) {
return sel
.filter((o) => isSegmentObjectType(o?.objectType) || /segment/i.test(o?.localizedTypeName || ""))
.map((o) => Number(o?.objectId ?? o?.id))
.filter((n) => Number.isFinite(n));
}
if (sel && typeof sel === "object") {
const type = sel.objectType ?? sel.type ?? sel.selectedObjectType;
const ids = sel.ids ?? sel.objectIds ?? sel.selectedIds;
if (isSegmentObjectType(type) && Array.isArray(ids)) {
return ids.map((x) => Number(x)).filter((n) => Number.isFinite(n));
}
if (Array.isArray(sel.selection)) return extractSegmentIdsFromSelection(sel.selection);
if (Array.isArray(sel.selectedItems)) return extractSegmentIdsFromSelection(sel.selectedItems);
if (Array.isArray(sel.objects)) return extractSegmentIdsFromSelection(sel.objects);
}
return [];
}
function selectedSegmentIds() {
try {
const raw = sdk?.Editing?.getSelection?.();
const ids = extractSegmentIdsFromSelection(raw);
if (ids.length) return Array.from(new Set(ids));
} catch {}
try {
const sm = UW?.W?.selectionManager;
const items = sm?.getSelectedFeatures?.() || sm?.getSelectedItems?.() || [];
if (Array.isArray(items)) {
const segIds = items
.map((x) => Number(x?.attributes?.id ?? x?.model?.attributes?.id ?? x?.id))
.filter((n) => Number.isFinite(n));
if (segIds.length) return Array.from(new Set(segIds));
}
} catch {}
return [];
}
// --- Place / Venue / POI selection detection (header only) ---
function _isPlaceObject(sel) {
try {
if (!sel) return false;
if (Array.isArray(sel)) return sel.some(_isPlaceObject);
if (typeof sel !== "object") return false;
const t = String(
sel.objectType ??
sel.type ??
sel.selectedObjectType ??
sel.localizedTypeName ??
sel.localizedType ??
sel.name ??
""
);
// Common labels across builds/locales
if (/venue|place|poi|point\s*of\s*interest/i.test(t)) return true;
// Explicit id hints
if ("venueId" in sel || "placeId" in sel || "poiId" in sel || "venueID" in sel || "placeID" in sel) return true;
// Numeric ObjectType constants when available
try {
const OT = sdk?.Editing?.ObjectType;
if (typeof sel.objectType === "number" && OT) {
const cand = [OT.VENUE, OT.PLACE, OT.POI, OT.POINT_OF_INTEREST].filter((v) => v != null);
if (cand.includes(sel.objectType)) return true;
}
} catch {}
// Nested selection containers
if (Array.isArray(sel.selection)) return _isPlaceObject(sel.selection);
if (Array.isArray(sel.selectedItems)) return _isPlaceObject(sel.selectedItems);
if (Array.isArray(sel.objects)) return _isPlaceObject(sel.objects);
if (Array.isArray(sel.items)) return _isPlaceObject(sel.items);
return false;
} catch {
return false;
}
}
function _extractPlaceIds(sel) {
try {
if (!sel) return [];
if (Array.isArray(sel)) {
return sel
.filter((o) => _isPlaceObject(o))
.map((o) => Number(o?.objectId ?? o?.id))
.filter((n) => Number.isFinite(n));
}
if (sel && typeof sel === "object") {
const type = sel.objectType ?? sel.type ?? sel.selectedObjectType;
const ids = sel.ids ?? sel.objectIds ?? sel.selectedIds;
if (_isPlaceObject({ objectType: type, localizedTypeName: sel.localizedTypeName, type: sel.type, selectedObjectType: sel.selectedObjectType }) && Array.isArray(ids)) {
return ids.map((x) => Number(x)).filter((n) => Number.isFinite(n));
}
if (Array.isArray(sel.selection)) return _extractPlaceIds(sel.selection);
if (Array.isArray(sel.selectedItems)) return _extractPlaceIds(sel.selectedItems);
if (Array.isArray(sel.objects)) return _extractPlaceIds(sel.objects);
if (Array.isArray(sel.items)) return _extractPlaceIds(sel.items);
}
} catch {}
return [];
}
function selectedPlaceInfo() {
// Returns: { has: boolean, ids: number[] }
try {
const raw = sdk?.Editing?.getSelection?.();
const ids = _extractPlaceIds(raw);
if (ids.length) return { has: true, ids: Array.from(new Set(ids)) };
if (_isPlaceObject(raw)) return { has: true, ids: [] };
} catch {}
// Best-effort legacy fallback (some builds don't expose venue ids in selection)
try {
const sm = UW?.W?.selectionManager;
const items = sm?.getSelectedFeatures?.() || sm?.getSelectedItems?.() || [];
if (Array.isArray(items) && items.length) {
for (const it of items) {
const a = it?.attributes || it?.model?.attributes || it?.data || null;
const hint = String(a?.type ?? a?.featureType ?? a?.objectType ?? it?.type ?? it?.name ?? "");
if (/venue|place|poi/i.test(hint)) return { has: true, ids: [] };
}
}
} catch {}
return { has: false, ids: [] };
}
// If an Update Request (UR) / Issue is active, let WME show its native right-click menu.
function isUpdateRequestSelection(sel) {
try {
if (!sel) return false;
if (Array.isArray(sel)) return sel.some(isUpdateRequestSelection);
if (typeof sel !== "object") return false;
const t = String(
sel.objectType ??
sel.type ??
sel.selectedObjectType ??
sel.localizedTypeName ??
sel.localizedType ??
sel.name ??
""
);
if (/update\s*request|updaterequest|\bur\b/i.test(t)) return true;
// Some builds use generic "issue"/"problem" types; look for UR-ish hints.
if (/issue|problem/i.test(t) && /update|request|\bur\b/i.test(t)) return true;
// Some selections expose explicit ids.
if ("updateRequestId" in sel || "urId" in sel || "issueId" in sel || "problemId" in sel) return true;
// Try numeric ObjectType constants if present.
try {
const OT = sdk?.Editing?.ObjectType;
if (typeof sel.objectType === "number" && OT) {
const cand = [OT.UPDATE_REQUEST, OT.UR, OT.MAP_PROBLEM, OT.ISSUE].filter((v) => v != null);
if (cand.includes(sel.objectType)) return true;
}
} catch {}
if (Array.isArray(sel.selection) && sel.selection.some(isUpdateRequestSelection)) return true;
if (Array.isArray(sel.selectedItems) && sel.selectedItems.some(isUpdateRequestSelection)) return true;
if (Array.isArray(sel.objects) && sel.objects.some(isUpdateRequestSelection)) return true;
} catch {}
return false;
}
function isUpdateRequestPanelOpen() {
// Robust, DOM-version-tolerant detection:
// If the UR panel is open, it almost always contains the action buttons below.
// We avoid broad `innerText` reads from generic "sidebar" nodes because WME's DOM varies across builds.
try {
const isVisible = (el) => {
try {
if (!el) return false;
const r = el.getBoundingClientRect?.();
if (!r) return true;
return r.width > 0 && r.height > 0;
} catch { return true; }
};
// 0) Fast path: the UR side-panel is a `wz-card` with class `mapUpdateRequest` (shadow DOM inside).
// The action buttons live in the open shadow root, so querying `button` from `document` won't see them.
// Detect the host element instead.
try {
const urHost = document.querySelector(
"wz-card.mapUpdateRequest, wz-card[class*='mapUpdateRequest'], wz-card[aria-label*='Update request'], wz-card[aria-label*='Αίτημα ενημέρωσης'], wz-card[aria-label*='Αιτημα ενημερωσης']"
);
if (urHost && isVisible(urHost)) return true;
} catch {}
const findBtn = (re) => {
try {
const nodes = document.querySelectorAll("button, [role='button']");
const limit = Math.min(nodes.length, 250);
for (let i = 0; i < limit; i++) {
const t = (nodes[i].textContent || "").trim();
if (t && re.test(t)) return nodes[i];
}
} catch {}
return null;
};
// Strong UR-specific UI controls (English + Greek best effort)
const btnSolved = findBtn(/^\s*mark\s+as\s+solved\s*$/i) || findBtn(/^\s*σήμανση\s+ως\s+λυμ(έ|ε)νο\s*$/i);
if (btnSolved && isVisible(btnSolved)) return true;
const btnNotId = findBtn(/^\s*mark\s+as\s+not\s+identified\s*$/i) || findBtn(/^\s*σήμανση\s+ως\s+μη\s+ταυτοποιημ(έ|ε)νο\s*$/i);
if (btnNotId && isVisible(btnNotId)) return true;
// Header title check (Update request / Αίτημα ενημέρωσης)
try {
const heads = document.querySelectorAll(
"#sidepanel [role='heading'], #sidebar [role='heading'], [class*='sidepanel'] [role='heading'], [class*='sidebar'] [role='heading'], " +
"#sidepanel h1, #sidepanel h2, #sidepanel h3, #sidebar h1, #sidebar h2, #sidebar h3"
);
const limit = Math.min(heads.length, 120);
for (let i = 0; i < limit; i++) {
const el = heads[i];
if (!isVisible(el)) continue;
const t = (el.textContent || "").trim();
if (!t) continue;
if (/^update\s*request(s)?$/i.test(t)) return true;
if (/^αίτημα\s+ενημέρωσης$/i.test(t) || /^αιτημα\s+ενημερωσης$/i.test(t)) return true;
}
} catch {}
// Fallback: narrow scan of likely panel containers (visible only)
try {
const roots = [
document.querySelector("#sidepanel"),
document.querySelector("#sidebar"),
document.querySelector("[class*='sidepanel']"),
document.querySelector("[class*='sidebar']"),
].filter(Boolean);
for (const root of roots) {
if (!isVisible(root)) continue;
const txt = (root.innerText || "").slice(0, 5000);
if (!txt) continue;
if (/update\s*request|αίτημα\s+ενημέρωσης|αιτημα\s+ενημερωσης/i.test(txt)) return true;
if (/mark\s+as\s+solved|mark\s+as\s+not\s+identified/i.test(txt)) return true;
}
} catch {}
} catch {}
return false;
}
function isUrMarkerAtPoint(x, y) {
try {
// elementFromPoint() can miss overlay icons when a transparent map layer sits on top.
// elementsFromPoint() gives us the full stack at the cursor.
const stack = (document.elementsFromPoint ? document.elementsFromPoint(x, y) : [document.elementFromPoint(x, y)]).filter(Boolean);
if (!stack || !stack.length) return false;
const urTextRe = /update\s*request|updaterequest|map[_\s-]?problem|problem\s*report|issue\s*report|user\s*report|\bur\b/i;
const markerHintRe = /marker|pin|icon|badge|bubble|olmarker|olalphaimg|overlay|layer|feature/i;
const hasUrSignals = (node) => {
if (!node || node.nodeType !== 1) return false;
// Strong signals: data-* ids that WME often uses for UR/problem objects.
try {
const ds = node.dataset || {};
if (ds.problemId || ds.issueId || ds.reportId || ds.updateRequestId || ds.urId) return true;
if (node.hasAttribute && (node.hasAttribute("data-problem-id") || node.hasAttribute("data-issue-id") || node.hasAttribute("data-report-id") || node.hasAttribute("data-update-request-id"))) return true;
} catch {}
const tag = (node.tagName || "").toUpperCase();
// Images/icons
if (tag === "IMG") {
const src = String(node.getAttribute("src") || "").toLowerCase();
const alt = String(node.getAttribute("alt") || "").toLowerCase();
if (src && (urTextRe.test(src) || /problem|issue|report|ur/.test(src))) return true;
if (alt && (urTextRe.test(alt) || /problem|issue|report|ur/.test(alt))) return true;
}
// SVG icons
if (tag === "SVG" || tag === "PATH" || tag === "G") {
const aria = String((node.getAttribute && (node.getAttribute("aria-label") || node.getAttribute("title") || "")) || "").toLowerCase();
if (aria && (urTextRe.test(aria) || /problem|issue|report|ur/.test(aria))) return true;
}
// Textual identifiers
const aria = String((node.getAttribute && (node.getAttribute("aria-label") || node.getAttribute("title") || node.getAttribute("role") || "")) || "").toLowerCase();
const cls = (typeof node.className === "string" ? node.className : "") || "";
const id = node.id || "";
const combo = (aria + " " + cls + " " + id).toLowerCase();
// UR-ish text + marker-ish hints, OR very strong "problem/issue/report" words on an icon element
if (combo) {
if (urTextRe.test(combo) && (markerHintRe.test(combo) || tag === "IMG" || tag === "SVG")) return true;
if (/problem|issue|report/.test(combo) && (markerHintRe.test(combo) || tag === "IMG" || tag === "SVG")) return true;
}
// CSS background icons
try {
const bg = (node.style && node.style.backgroundImage) ? String(node.style.backgroundImage).toLowerCase() : "";
if (bg && (urTextRe.test(bg) || /problem|issue|report|ur/.test(bg))) return true;
} catch {}
return false;
};
// Check the stack + a few ancestor levels for each element.
for (const el of stack.slice(0, 20)) {
let cur = el;
for (let i = 0; cur && i < 8; i++) {
if (hasUrSignals(cur)) return true;
cur = cur.parentElement;
}
}
} catch {}
return false;
}
function isInUpdateRequestPanel(target) {
try {
if (!target || !target.closest) return false;
// Primary: the UR panel host is a <wz-card> with class "mapUpdateRequest" (Shadow DOM inside).
const host = target.closest("wz-card.mapUpdateRequest, .mapUpdateRequest");
if (host) return true;
// Fallbacks: some builds rely on aria labels / testids.
const host2 = target.closest(
"wz-card[aria-label*='Update request'], wz-card[aria-label*='Update Request'], wz-card[aria-label*='Αίτημα'], " +
"[data-testid*='update'], [data-testid*='problem'], [data-testid*='mapUpdateRequest']"
);
if (host2) return true;
} catch {}
return false;
}
function shouldAllowNativeContextMenu(e) {
try {
// Shift+RightClick always yields WME native menu.
if (e && e.shiftKey) return true;
// Always allow WME native menu inside the Update Request (UR) sidebar panel.
if (e && isInUpdateRequestPanel(e.target)) return true;
// Native menu ONLY when the right-click is actually on a UR marker.
if (e && isUrMarkerAtPoint(e.clientX, e.clientY)) return true;
// Weak fallback: if selection is an UR and the element under cursor still looks marker-ish.
// This avoids "sticking" native mode when you right-click empty map while UR panel is open.
try {
const sel = (() => { try { return sdk?.Editing?.getSelection?.(); } catch { return null; } })();
if (isUpdateRequestSelection(sel) && e) {
const el = document.elementFromPoint(e.clientX, e.clientY);
const hint = ((el?.className || "") + " " + (el?.id || "")).toLowerCase();
if (/marker|pin|icon|olmarker|olalphaimg/.test(hint)) return true;
}
} catch {}
} catch {}
return false;
}
function fmt(n) { return Number(n).toFixed(6); }
function escapeHtml(s) {
return String(s)
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
async function setClipboard(text) {
try { if (typeof GM_setClipboard === "function") { GM_setClipboard(text); return true; } } catch {}
try { await navigator.clipboard.writeText(text); return true; } catch {}
try {
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
const ok = document.execCommand("copy");
ta.remove();
return ok;
} catch {}
return false;
}
function detectRankBase() {
if (rankIsZeroBased !== null) return;
rankIsZeroBased = !!(UW?.W?.loginManager?.user?.getRank);
}
function normalizeToLevel(raw) {
if (!Number.isFinite(raw)) return null;
detectRankBase();
if (raw === 0) return 1;
if (rankIsZeroBased && raw >= 0 && raw <= 10) return raw + 1;
return raw;
}
function readLevelFromW() {
try {
const u = UW?.W?.loginManager?.user;
if (typeof u?.getRank === "function") {
const r = u.getRank();
if (Number.isFinite(r)) return r + 1;
}
const candidates = [
u?.level, u?.attributes?.level,
u?.rank, u?.attributes?.rank,
UW?.W?.user?.level, UW?.W?.user?.rank,
UW?.W?.app?.user?.level, UW?.W?.app?.user?.rank,
].map((x) => Number(x)).filter((x) => Number.isFinite(x));
for (const c of candidates) {
const lvl = normalizeToLevel(c);
if (Number.isFinite(lvl)) return lvl;
}
} catch {}
return null;
}
function readLevelFromSdk() {
try {
const a = sdk?.UserSession?.getUserInfo?.()?.rank;
const lvlA = normalizeToLevel(Number(a));
if (Number.isFinite(lvlA)) return lvlA;
} catch {}
try {
const b = sdk?.WmeState?.getUserInfo?.()?.rank;
const lvlB = normalizeToLevel(Number(b));
if (Number.isFinite(lvlB)) return lvlB;
} catch {}
return null;
}
function getUserLevel() {
if (Number.isFinite(cachedUserLevel)) return cachedUserLevel;
const fromW = readLevelFromW();
if (Number.isFinite(fromW)) return (cachedUserLevel = fromW);
const fromSdk = readLevelFromSdk();
if (Number.isFinite(fromSdk)) return (cachedUserLevel = fromSdk);
return null;
}
function decideLockBase(sampleLockRank, userLevel) {
if (lockIsZeroBased !== null) return lockIsZeroBased;
if (Number.isFinite(sampleLockRank) && sampleLockRank >= 0 && sampleLockRank <= 6) { lockIsZeroBased = true; return true; }
if (Number.isFinite(sampleLockRank) && Number.isFinite(userLevel) && userLevel >= 2) {
if (sampleLockRank === userLevel - 1) { lockIsZeroBased = true; return true; }
}
lockIsZeroBased = false;
return false;
}
function lockRankToLevel(lockRank, userLevelMaybe) {
if (!Number.isFinite(lockRank)) return null;
return decideLockBase(lockRank, userLevelMaybe) ? lockRank + 1 : lockRank;
}
function levelToLockRank(level, lockRankSample, userLevelMaybe) {
if (!Number.isFinite(level)) return null;
return decideLockBase(lockRankSample, userLevelMaybe) ? level - 1 : level;
}
function sdkSegGetById(segmentId) {
return (
sdk?.Segments?.getById?.({ segmentId }) ??
sdk?.DataModel?.Segments?.getById?.({ segmentId }) ??
null
);
}
function sdkStreetsGetById(streetId) {
return (
sdk?.Streets?.getById?.({ streetId }) ??
sdk?.DataModel?.Streets?.getById?.({ streetId }) ??
null
);
}
function sdkSegUpdateSegment(segmentId, attrs) {
if (typeof sdk?.Segments?.updateSegment === "function") {
return sdk.Segments.updateSegment({ segmentId, ...attrs });
}
if (typeof sdk?.DataModel?.Segments?.updateSegment === "function") {
return sdk.DataModel.Segments.updateSegment({ segmentId, ...attrs });
}
throw new Error("No SDK Segments.updateSegment available.");
}
function segWGetById(id) {
try {
const n = Number(id);
const segs = UW?.W?.model?.segments;
if (!segs || typeof segs.getObjectById !== "function") return null;
return segs.getObjectById(n) || null;
} catch {
return null;
}
}
function wAddAction(action) {
try {
const am = UW?.W?.model?.actionManager;
if (!am || typeof am.add !== "function") return false;
am.add(action);
return true;
} catch {
return false;
}
}
function wUpdateObject(modelObj, attrs) {
try {
const A = UW?.W?.Action;
if (!A) return false;
const C =
A.UpdateObject ||
A.UpdateModelObject ||
A.UpdateSegment ||
A.UpdateSegmentAttributes ||
null;
if (typeof C !== "function") return false;
try {
return wAddAction(new C(modelObj, attrs));
} catch {
return wAddAction(new C(modelObj, modelObj?.attributes || {}, attrs));
}
} catch {
return false;
}
}
function wSetSegmentDirection(segW, targetMode) {
const fwd = targetMode === "2" || targetMode === "A";
const rev = targetMode === "2" || targetMode === "B";
const attrs = { fwdDirection: fwd, revDirection: rev, isAtoB: fwd, isBtoA: rev };
return wUpdateObject(segW, attrs);
}
function wReverseSegmentDirection(segW) {
try {
const A = UW?.W?.Action;
if (!A) return false;
const names = [
"ReverseSegmentDirection",
"ReverseSegmentsDirection",
"ReverseSegment",
"ReverseSegmentGeometry",
];
for (const n of names) {
const C = A[n];
if (typeof C !== "function") continue;
try {
if (wAddAction(new C(segW))) return true;
} catch {}
try {
if (wAddAction(new C(segW?.attributes?.id ?? segW?.id))) return true;
} catch {}
}
return false;
} catch {
return false;
}
}
function getAllLoadedSegmentsW() {
try {
const segs = UW?.W?.model?.segments;
if (!segs) return [];
if (typeof segs.getObjectArray === "function") return segs.getObjectArray() || [];
if (segs.objects && typeof segs.objects === "object") return Object.values(segs.objects).filter(Boolean);
} catch {}
return [];
}
function segIdFromW(segW) {
return Number(segW?.attributes?.id ?? segW?.id ?? (typeof segW?.getID === "function" ? segW.getID() : NaN));
}
function primaryStreetIdFromW(segW) {
const a = segW?.attributes || {};
return a.primaryStreetID ?? a.primaryStreetId ?? a.primaryStreet ?? segW?.primaryStreetId ?? null;
}
function lockRankFromW(segW) {
const a = segW?.attributes || {};
const v = a.lockRank ?? segW?.lockRank;
return Number.isFinite(Number(v)) ? Number(v) : null;
}
function dirFromW(segW) {
const a = segW?.attributes || {};
const isAtoB = (typeof segW?.isAtoB === "boolean") ? segW.isAtoB :
(typeof a.isAtoB === "boolean") ? a.isAtoB :
(typeof a.fwdDirection === "boolean") ? a.fwdDirection : null;
const isBtoA = (typeof segW?.isBtoA === "boolean") ? segW.isBtoA :
(typeof a.isBtoA === "boolean") ? a.isBtoA :
(typeof a.revDirection === "boolean") ? a.revDirection : null;
const twoWay = (isAtoB === true && isBtoA === true);
if (twoWay) return { mode: "2", isAtoB: true, isBtoA: true };
if (isAtoB === true && isBtoA === false) return { mode: "A", isAtoB: true, isBtoA: false };
if (isAtoB === false && isBtoA === true) return { mode: "B", isAtoB: false, isBtoA: true };
return { mode: "?", isAtoB, isBtoA };
}
function speedFromW(segW) {
const a = segW?.attributes || {};
const f = a.fwdSpeedLimit ?? segW?.fwdSpeedLimit;
const r = a.revSpeedLimit ?? segW?.revSpeedLimit;
const nf = (f == null ? null : Number(f));
const nr = (r == null ? null : Number(r));
return { f: Number.isFinite(nf) ? nf : null, r: Number.isFinite(nr) ? nr : null };
}
function elevationFromW(segW) {
const a = segW?.attributes || {};
const v = a.level ?? a.elevation ?? segW?.level ?? segW?.elevation;
const n = (v == null ? null : Number(v));
return Number.isFinite(n) ? n : null;
}
function getSegLevelById(segmentId) {
try {
const seg = sdkSegGetById(segmentId);
if (Number.isFinite(seg?.level)) return Number(seg.level);
if (Number.isFinite(seg?.elevation)) return Number(seg.elevation);
if (seg?.attributes && Number.isFinite(seg.attributes.level)) return Number(seg.attributes.level);
if (seg?.attributes && Number.isFinite(seg.attributes.elevation)) return Number(seg.attributes.elevation);
} catch {}
try {
const segW = UW?.W?.model?.segments?.getObjectById?.(Number(segmentId));
return segW ? elevationFromW(segW) : null;
} catch {}
return null;
}
function setSegLevelById(segmentId, level) {
const lvl = (level == null ? null : Number(level));
if (!Number.isFinite(lvl)) return false;
try {
sdkSegUpdateSegment(segmentId, { level: lvl });
return true;
} catch {}
try {
const segW = UW?.W?.model?.segments?.getObjectById?.(Number(segmentId));
if (!segW) return false;
const cur = elevationFromW(segW);
if (cur === lvl) return true;
const AM = UW?.W?.model?.actionManager;
const WA = UW?.W?.Action;
if (AM && WA) {
if (typeof WA.UpdateObject === "function") { AM.add(new WA.UpdateObject(segW, { level: lvl })); return true; }
if (typeof WA.UpdateSegment === "function") { AM.add(new WA.UpdateSegment(segW, { level: lvl })); return true; }
}
if (typeof segW.setAttribute === "function") { segW.setAttribute("level", lvl); return true; }
if (segW.attributes) { segW.attributes.level = lvl; return true; }
} catch {}
return false;
}
function setSelectionToSegmentIds(ids) {
const uniq = Array.from(new Set((ids || []).map(Number).filter(Number.isFinite)));
if (!uniq.length) { toast("Nothing to select (not loaded)."); return false; }
try {
if (sdk?.Editing?.ObjectType?.SEGMENT != null && typeof sdk?.Editing?.setSelection === "function") {
sdk.Editing.setSelection({
selection: uniq.map((id) => ({ objectType: sdk.Editing.ObjectType.SEGMENT, objectId: id })),
});
toast(`Selected ${uniq.length} segment(s)`);
return true;
}
} catch {}
try {
const sm = UW?.W?.selectionManager;
const segs = UW?.W?.model?.segments;
if (sm && segs && typeof segs.getObjectById === "function") {
const models = uniq.map((id) => segs.getObjectById(id)).filter(Boolean);
if (models.length) {
if (typeof sm.setSelectedModels === "function") {
sm.setSelectedModels(models);
toast(`Selected ${models.length} segment(s)`);
return true;
}
if (typeof sm.setSelectedItems === "function") {
sm.setSelectedItems(models);
toast(`Selected ${models.length} segment(s)`);
return true;
}
}
}
} catch {}
toast("Selection API not available (WME changed).");
return false;
}
function setSelectionToSegmentIdsSilent(ids) {
const uniq = Array.from(new Set((ids || []).map(Number).filter(Number.isFinite)));
if (!uniq.length) return false;
try {
if (sdk?.Editing?.ObjectType?.SEGMENT != null && typeof sdk?.Editing?.setSelection === "function") {
sdk.Editing.setSelection({
selection: uniq.map((id) => ({ objectType: sdk.Editing.ObjectType.SEGMENT, objectId: id })),
});
return true;
}
} catch {}
try {
const sm = UW?.W?.selectionManager;
const segs = UW?.W?.model?.segments;
if (sm && segs && typeof segs.getObjectById === "function") {
const models = uniq.map((id) => segs.getObjectById(id)).filter(Boolean);
if (models.length) {
if (typeof sm.setSelectedModels === "function") { sm.setSelectedModels(models); return true; }
if (typeof sm.setSelectedItems === "function") { sm.setSelectedItems(models); return true; }
}
}
} catch {}
return false;
}
function getSegmentsLockInfo(segIds) {
const locks = [];
for (const id of segIds) {
const seg = sdkSegGetById(id);
const lk = seg?.lockRank;
if (Number.isFinite(lk)) locks.push(lk);
}
if (!locks.length) return { currentRaw: null, currentLevel: null, mixed: false };
const first = locks[0];
const mixed = locks.some((x) => x !== first);
const userLevel = getUserLevel();
const lvl = mixed ? null : lockRankToLevel(first, userLevel);
return { currentRaw: first, currentLevel: lvl, mixed };
}
async function setLockForSelection(segIds, targetLevel) {
const userLevel = getUserLevel() ?? 1;
const lockInfo = getSegmentsLockInfo(segIds);
const rawToSet = levelToLockRank(targetLevel, lockInfo.currentRaw, userLevel);
if (!Number.isFinite(rawToSet)) throw new Error("Could not compute lock rank to set.");
let ok = 0, fail = 0;
for (const id of segIds) {
try { await sdkSegUpdateSegment(id, { lockRank: rawToSet }); ok++; }
catch { fail++; }
}
toast(`Lock ${targetLevel} • ${ok}/${segIds.length}${fail ? `, ${fail} failed` : ""}`);
updateRootRowSub("lock", fail ? "mixed" : `L${targetLevel}`);
refreshActiveSubmenuIf("lock");
}
function buildLockSubmenu(segIds) {
const userLevel = getUserLevel() ?? 1;
const maxLevel = Math.min(6, Math.max(1, userLevel));
const lockInfo = getSegmentsLockInfo(segIds);
const items = [
{ label: withIcon(ICONS.lock, "Lock level"), sub: lockInfo.mixed ? "mixed" : `current L${lockInfo.currentLevel ?? "?"}`, disabled: true },
{ type: "sep" },
];
for (let lvl = 1; lvl <= maxLevel; lvl++) {
const isCurrent = (!lockInfo.mixed && lockInfo.currentLevel != null && lockInfo.currentLevel === lvl);
items.push({
label: `Lock ${lvl}`,
selected: isCurrent,
check: isCurrent,
onClick: () => setLockForSelection(segIds, lvl),
});
}
return items;
}
function dirSummaryForSelection(segIds) {
const modes = [];
for (const id of segIds) {
const seg = sdkSegGetById(id);
if (!seg) continue;
const is2 = !!seg.isTwoWay;
const isA = !!seg.isAtoB;
const isB = !!seg.isBtoA;
if (is2) modes.push("2");
else if (isA) modes.push("A");
else if (isB) modes.push("B");
else if (seg.allowNoDirection) modes.push("N");
else modes.push("?");
}
if (!modes.length) return { mode: "?", mixed: false };
const first = modes[0];
const mixed = modes.some((m) => m !== first);
return { mode: mixed ? "M" : first, mixed };
}
function modeLabel(mode) {
if (mode === "2") return "2-way";
if (mode === "A") return "A→B";
if (mode === "B") return "B→A";
if (mode === "N") return "no-dir";
if (mode === "M") return "mixed";
return "unknown";
}
async function setDirectionForSelection(segIds, targetMode) {
let ok = 0, fail = 0;
const dir = targetMode === "2" ? "TWO_WAY" : targetMode === "A" ? "A_TO_B" : targetMode === "B" ? "B_TO_A" : null;
for (const id of segIds) {
let done = false;
if (dir) {
try {
await sdkSegUpdateSegment(id, { direction: dir });
done = true;
} catch {}
}
if (!done) {
const segW = segWGetById(id);
if (segW && wSetSegmentDirection(segW, targetMode)) done = true;
}
if (done) ok++;
else fail++;
}
toast(`Direction → ${modeLabel(targetMode)} • ${ok}/${segIds.length}${fail ? `, ${fail} failed` : ""}`);
updateRootRowSub("dir", fail ? "mixed" : modeLabel(targetMode));
refreshActiveSubmenuIf("dir");
}
async function flipDirectionForSelection(segIds) {
let ok = 0, fail = 0;
for (const id of segIds) {
let done = false;
const segW = segWGetById(id);
if (segW && wReverseSegmentDirection(segW)) done = true;
if (!done) {
try {
const seg = sdkSegGetById(id);
if (!seg) throw new Error("missing");
const next = seg.isAtoB && !seg.isBtoA ? "B_TO_A" : seg.isBtoA && !seg.isAtoB ? "A_TO_B" : null;
if (!next) throw new Error("not flippable");
await sdkSegUpdateSegment(id, { direction: next });
done = true;
} catch {}
}
if (done) ok++;
else fail++;
}
toast(`Flipped direction • ${ok}/${segIds.length}${fail ? `, ${fail} failed` : ""}`);
const info = dirSummaryForSelection(segIds);
updateRootRowSub("dir", info.mixed ? "mixed" : modeLabel(info.mode));
refreshActiveSubmenuIf("dir");
}
function buildDirectionSubmenu(segIds) {
const info = dirSummaryForSelection(segIds);
const cur = info.mixed ? "M" : info.mode;
return [
{ label: withIcon(ICONS.arrows, "Direction"), sub: `current: ${modeLabel(cur)}`, disabled: true },
{ type: "sep" },
{ label: withIcon(ICONS.arrows, "Flip direction"), sub: "Swap A→B / B→A (2-way unchanged)", onClick: () => flipDirectionForSelection(segIds) },
{ type: "sep" },
{ label: "Make 2-way", sub: "A→B + B→A", selected: (!info.mixed && info.mode === "2"), check: (!info.mixed && info.mode === "2"), onClick: () => setDirectionForSelection(segIds, "2") },
{ label: "Make 1-way A→B", sub: "Forward only", selected: (!info.mixed && info.mode === "A"), check: (!info.mixed && info.mode === "A"), onClick: () => setDirectionForSelection(segIds, "A") },
{ label: "Make 1-way B→A", sub: "Reverse only", selected: (!info.mixed && info.mode === "B"), check: (!info.mixed && info.mode === "B"), onClick: () => setDirectionForSelection(segIds, "B") },
];
}
function getSegDirectionMode(seg) {
if (!seg) return "BOTH";
if (seg.isTwoWay) return "BOTH";
if (seg.isAtoB && !seg.isBtoA) return "FWD";
if (seg.isBtoA && !seg.isAtoB) return "REV";
return "BOTH";
}
function getSegmentsSpeedInfo(segIds) {
let any = 0, firstKey = null, mixed = false;
for (const id of segIds) {
const seg = sdkSegGetById(id);
if (!seg) continue;
any++;
const mode = getSegDirectionMode(seg);
const f = seg.fwdSpeedLimit;
const r = seg.revSpeedLimit;
const key =
mode === "FWD" ? `F:${f ?? "∅"}` :
mode === "REV" ? `R:${r ?? "∅"}` :
`B:${(f ?? "∅")}/${(r ?? "∅")}`;
if (firstKey === null) firstKey = key;
else if (firstKey !== key) mixed = true;
if (mixed) break;
}
if (!any) return { summary: "unknown", mixed: true };
if (mixed) return { summary: "mixed", mixed: true };
const seg0 = sdkSegGetById(segIds[0]);
const mode0 = getSegDirectionMode(seg0);
if (mode0 === "FWD") return { summary: seg0?.fwdSpeedLimit == null ? "none" : `${seg0.fwdSpeedLimit}`, mixed: false };
if (mode0 === "REV") return { summary: seg0?.revSpeedLimit == null ? "none" : `${seg0.revSpeedLimit}`, mixed: false };
const a = seg0?.fwdSpeedLimit;
const b = seg0?.revSpeedLimit;
return { summary: `${a ?? "∅"}/${b ?? "∅"}`, mixed: false };
}
function getCurrentSpeedValueForChoice(segIds, choice) {
const seg0 = sdkSegGetById(segIds[0]);
if (!seg0) return { value: null, mixed: true };
const info = getSegmentsSpeedInfo(segIds);
if (info.mixed) return { value: null, mixed: true };
const mode = getSegDirectionMode(seg0);
if (mode !== "BOTH") {
const v = (mode === "FWD") ? seg0.fwdSpeedLimit : seg0.revSpeedLimit;
return { value: (v == null ? null : Number(v)), mixed: false };
}
if (choice === "FWD") return { value: seg0.fwdSpeedLimit == null ? null : Number(seg0.fwdSpeedLimit), mixed: false };
if (choice === "REV") return { value: seg0.revSpeedLimit == null ? null : Number(seg0.revSpeedLimit), mixed: false };
const f = seg0.fwdSpeedLimit, r = seg0.revSpeedLimit;
if (f == null && r == null) return { value: null, mixed: false };
if (f != null && r != null && Number(f) === Number(r)) return { value: Number(f), mixed: false };
return { value: null, mixed: true };
}
async function setSpeedForSelection(segIds, valueOrNull, modeOverride = "AUTO", keepOpen = false) {
let ok = 0, fail = 0;
for (const id of segIds) {
const seg = sdkSegGetById(id);
if (!seg) { fail++; continue; }
const mode = getSegDirectionMode(seg);
const patch = {};
if (mode !== "BOTH") {
if (mode === "FWD") patch.fwdSpeedLimit = valueOrNull;
else patch.revSpeedLimit = valueOrNull;
} else {
const eff = (modeOverride === "AUTO" ? "BOTH" : modeOverride);
if (eff === "FWD") patch.fwdSpeedLimit = valueOrNull;
else if (eff === "REV") patch.revSpeedLimit = valueOrNull;
else { patch.fwdSpeedLimit = valueOrNull; patch.revSpeedLimit = valueOrNull; }
}
try { sdkSegUpdateSegment(id, patch); ok++; }
catch { fail++; }
}
const label = valueOrNull == null ? "cleared" : `→ ${valueOrNull}`;
const suffix = (modeOverride !== "AUTO") ? ` (${modeOverride === "BOTH" ? "Both" : modeOverride === "FWD" ? "A→B" : "B→A"})` : "";
toast(`Speed ${label}${suffix} • ${ok}/${segIds.length}${fail ? `, ${fail} failed` : ""}`);
if (keepOpen) {
refreshActiveSubmenuIf("speed");
const speedSummary = getSegmentsSpeedInfo(segIds).summary;
updateRootRowSub("speed", speedSummary);
return;
}
closeAllMenus();
}
function buildSpeedDirectionChips(isTwoWay, segIds) {
if (!isTwoWay) return null;
const wrap = document.createElement("div");
wrap.className = "wmeRcChips";
const mk = (id, label) => {
const b = document.createElement("div");
b.className = "wmeRcChip" + (speedDirChoice === id ? " on" : "");
b.textContent = label;
b.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
speedDirChoice = id;
refreshActiveSubmenuIf("speed");
const speedSummary = getSegmentsSpeedInfo(segIds).summary;
updateRootRowSub("speed", speedSummary);
});
return b;
};
wrap.appendChild(mk("BOTH", "Both"));
wrap.appendChild(mk("FWD", "A→B"));
wrap.appendChild(mk("REV", "B→A"));
return wrap;
}
function buildSpeedSubmenu(segIds) {
const seg0 = sdkSegGetById(segIds[0]);
const isTwoWay = !!seg0?.isTwoWay;
const speeds = [20,30,40,50,60,70,80,90,100,110,120,130];
const cur = getCurrentSpeedValueForChoice(segIds, speedDirChoice);
const curTxt = cur.mixed ? "current: mixed" : (cur.value == null ? "current: none" : `current: ${cur.value}`);
const chips = buildSpeedDirectionChips(isTwoWay, segIds);
return [{
type: "speedGrid",
titleLeft: isTwoWay ? "Speed limit • choose direction" : "Speed limit",
titleRight: curTxt,
chipsEl: chips,
buttons: [
...speeds.map((s) => ({
text: String(s),
title: `${s} km/h`,
selected: (!cur.mixed && cur.value != null && Number(cur.value) === s),
onClick: () => setSpeedForSelection(segIds, s, isTwoWay ? speedDirChoice : "AUTO", true),
})),
{
html: ICONS.trash,
title: "Clear speed",
selected: (!cur.mixed && cur.value == null),
onClick: () => setSpeedForSelection(segIds, null, isTwoWay ? speedDirChoice : "AUTO", true),
},
],
}];
}
function segmentCoords(segmentId) {
const seg = sdkSegGetById(segmentId);
const c = seg?.geometry?.coordinates;
if (!Array.isArray(c)) return [];
return c
.filter((p) => Array.isArray(p) && p.length >= 2)
.map((p) => [Number(p[0]), Number(p[1])])
.filter(([lon, lat]) => Number.isFinite(lon) && Number.isFinite(lat));
}
function bboxFromSegments(segIds) {
const coords = [];
for (const id of segIds) coords.push(...segmentCoords(id));
if (!coords.length) return null;
let left = Infinity, bottom = Infinity, right = -Infinity, top = -Infinity;
for (const [lon, lat] of coords) {
if (lon < left) left = lon;
if (lon > right) right = lon;
if (lat < bottom) bottom = lat;
if (lat > top) top = lat;
}
return isFinite(left) ? [left, bottom, right, top] : null;
}
async function zoomToSegments(segIds) {
const bbox = bboxFromSegments(segIds);
if (!bbox || typeof sdk?.Map?.zoomToExtent !== "function") throw new Error("Zoom unavailable.");
sdk.Map.zoomToExtent({ bbox });
}
async function actionZoomTo(segIds) {
try { await zoomToSegments(segIds); toast("Zoomed."); }
catch { toast("Zoom unavailable."); }
closeAllMenus();
}
function zoomToLonLat(lon, lat) {
let moved = false;
try {
if (typeof sdk?.Map?.zoomToExtent === "function") {
const d = 0.0009;
sdk.Map.zoomToExtent({ bbox: [lon - d, lat - d, lon + d, lat + d] });
moved = true;
}
} catch {}
if (!moved) {
try {
if (typeof sdk?.Map?.setCenter === "function") {
sdk.Map.setCenter({ lon, lat });
moved = true;
}
} catch {}
}
/* visible-area correction disabled */
return moved;
}
function zoomToLonLatExact(lon, lat, zoomLevel) {
let moved = false;
const z = Number.isFinite(Number(zoomLevel)) ? Number(zoomLevel) : null;
try {
if (typeof sdk?.Map?.setCenter === "function") {
sdk.Map.setCenter({ lon, lat });
if (Number.isFinite(z) && typeof sdk?.Map?.setZoom === "function") {
sdk.Map.setZoom({ zoomLevel: z });
}
requestAnimationFrame(() => {
try {
sdk?.Map?.setCenter?.({ lon, lat });
if (Number.isFinite(z)) sdk?.Map?.setZoom?.({ zoomLevel: z });
} catch {}
});
moved = true;
}
} catch {}
if (!moved) {
try {
const map = getOlMapBestEffort();
const ol = UW?.OpenLayers;
if (map && ol && typeof map.setCenter === "function") {
let ll = new ol.LonLat(lon, lat);
try {
const dst = map.getProjectionObject?.() || map.projection || null;
const dstCode = String(dst?.projCode || dst?.getCode?.() || dst || "");
const needsTransform = /900913|3857|102113|102100/i.test(dstCode) && !/4326/i.test(dstCode);
if (needsTransform && typeof ll.transform === "function") {
const src = new ol.Projection("EPSG:4326");
if (dst) ll.transform(src, dst);
}
} catch {}
map.setCenter(ll, Number.isFinite(z) ? z : undefined);
moved = true;
}
} catch {}
}
if (!moved) {
try {
if (typeof sdk?.Map?.zoomToExtent === "function") {
const d = 0.0009;
sdk.Map.zoomToExtent({ bbox: [lon - d, lat - d, lon + d, lat + d] });
moved = true;
}
} catch {}
}
return moved;
}
function computeDxDyToCenter(map, lon, lat) {
try {
const ol = UW?.OpenLayers;
if (!map || !ol) return null;
if (typeof map.getLayerPxFromLonLat !== "function" || typeof map.getSize !== "function") return null;
let ll = new ol.LonLat(lon, lat);
try {
const dst = map.getProjectionObject?.() || map.projection || null;
const dstCode = String(dst?.projCode || dst?.getCode?.() || dst || "");
const needsTransform = /900913|3857|102113|102100/i.test(dstCode) && !/4326/i.test(dstCode);
if (needsTransform && typeof ll.transform === "function") {
const src = new ol.Projection("EPSG:4326");
if (dst) ll.transform(src, dst);
}
} catch {}
const px = map.getLayerPxFromLonLat(ll);
const sz = map.getSize();
if (!px || !sz || !isFinite(px.x) || !isFinite(px.y) || !isFinite(sz.w) || !isFinite(sz.h)) return null;
const dx = Math.round((sz.w / 2) - px.x);
const dy = Math.round((sz.h / 2) - px.y);
return { dx, dy };
} catch {}
return null;
}
function ensureCenteredAfterMotion(lon, lat) {
let attempt = 0;
const maxAttempts = 6;
const tick = () => {
try {
const map = getOlMapBestEffort();
const r = computeDxDyToCenter(map, lon, lat);
if (!r) return;
const { dx, dy } = r;
if (Math.abs(dx) <= 1 && Math.abs(dy) <= 1) return;
panByPx(dx, dy);
} catch {}
attempt += 1;
if (attempt <= maxAttempts) {
setTimeout(tick, 90 + attempt * 70);
}
};
requestAnimationFrame(() => requestAnimationFrame(tick));
}
let _pinJumpWarmupDone = false;
function getDefaultPinZoom() {
return 17;
}
function centerPinInstant(lon, lat, zoomLevel) {
const z = Number.isFinite(Number(zoomLevel)) ? Number(zoomLevel) : null;
try {
const map = getOlMapBestEffort();
const ol = UW?.OpenLayers;
if (map && ol && typeof map.setCenter === "function") {
let ll = new ol.LonLat(lon, lat);
try {
const dst = map.getProjectionObject?.() || map.projection || null;
const dstCode = String(dst?.projCode || dst?.getCode?.() || dst || "");
const needsTransform = /900913|3857|102113|102100/i.test(dstCode) && !/4326/i.test(dstCode);
if (needsTransform && typeof ll.transform === "function") {
const src = new ol.Projection("EPSG:4326");
if (dst) ll.transform(src, dst);
}
} catch {}
map.setCenter(ll, Number.isFinite(z) ? z : undefined, true, true);
try {
const dx = computeVisibleCenterPanDx();
if (dx && typeof map.getViewPortPxFromLonLat === "function" && typeof map.getLonLatFromViewPortPx === "function") {
const cpx = map.getViewPortPxFromLonLat(map.getCenter());
if (cpx) {
const newPx = new ol.Pixel(cpx.x - dx, cpx.y);
const newCenter = map.getLonLatFromViewPortPx(newPx);
if (newCenter) map.setCenter(newCenter, Number.isFinite(z) ? z : undefined, true, true);
}
}
} catch {}
return true;
}
} catch {}
try {
if (typeof sdk?.Map?.setCenter === "function") {
sdk.Map.setCenter({ lon, lat });
if (Number.isFinite(z) && typeof sdk?.Map?.setZoom === "function") sdk.Map.setZoom({ zoomLevel: z });
return true;
}
} catch {}
try {
if (typeof sdk?.Map?.zoomToExtent === "function") {
const d = 0.0009;
sdk.Map.zoomToExtent({ bbox: [lon - d, lat - d, lon + d, lat + d] });
return true;
}
} catch {}
return false;
}
function jumpToPin(pin) {
try {
if (!pin) return;
const z = getDefaultPinZoom();
try {
const map = getOlMapBestEffort();
map && map.updateSize && map.updateSize();
} catch {}
centerPinInstant(pin.lon, pin.lat, z);
if (!_pinJumpWarmupDone) {
_pinJumpWarmupDone = true;
const warm = () => {
try {
const map = getOlMapBestEffort();
map && map.updateSize && map.updateSize();
} catch {}
centerPinInstant(pin.lon, pin.lat, z);
};
setTimeout(warm, 260);
setTimeout(warm, 820);
}
} catch (e) {
try { console.error(e); } catch {}
try { toast("Pin: jump failed."); } catch {}
}
} function applyVisibleCenterOffset() {
const dx = computeVisibleCenterPanDx();
if (!dx) return;
requestAnimationFrame(() => requestAnimationFrame(() => { panByPx(dx, 0); }));
}
function computeVisibleCenterPanDx() {
const left = measureSideObstructionPx("left");
const right = measureSideObstructionPx("right");
const net = (left - right) / 2;
const dx = -Math.round(net);
return Math.abs(dx) >= 10 ? dx : 0;
}
function measureSideObstructionPx(side) {
const mapEl = document.querySelector("#map") || document.querySelector(".olMap") || null;
const mapRect = mapEl ? mapEl.getBoundingClientRect() : { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight, width: window.innerWidth, height: window.innerHeight };
const x = side === "left" ? Math.max(2, mapRect.left + 4) : Math.min(window.innerWidth - 2, mapRect.right - 4);
const y = Math.min(window.innerHeight - 2, Math.max(2, mapRect.top + Math.min(360, mapRect.height * 0.35)));
let stack = [];
try { stack = document.elementsFromPoint(x, y) || []; } catch {}
for (const el of stack) {
if (!el || el === document.documentElement || el === document.body) continue;
if (mapEl && (el === mapEl || mapEl.contains(el))) continue;
const r = el.getBoundingClientRect();
if (r.width < 140 || r.height < 200) continue;
if (r.bottom <= mapRect.top + 10 || r.top >= mapRect.bottom - 10) continue;
const overlapsHoriz = !(r.right <= mapRect.left + 2 || r.left >= mapRect.right - 2);
if (!overlapsHoriz) continue;
const cs = getComputedStyle(el);
if (cs.display === "none" || cs.visibility === "hidden") continue;
if (Number(cs.opacity) === 0) continue;
if (side === "left") {
const covered = Math.max(0, Math.min(r.right, mapRect.right) - mapRect.left);
return covered;
}
const covered = Math.max(0, mapRect.right - Math.max(r.left, mapRect.left));
return covered;
}
return 0;
}
function panByPx(dx, dy) {
dx = Math.round(dx || 0);
dy = Math.round(dy || 0);
if (!dx && !dy) return false;
try {
const olMap = UW?.W?.map;
if (olMap) {
if (typeof olMap.pan === "function") { olMap.pan(dx, dy, { animate: false }); return true; }
if (typeof olMap.moveByPx === "function") { olMap.moveByPx(dx, dy); return true; }
}
} catch {}
try {
const getLL = sdk?.Map?.getLonLatFromPixel;
const setC = sdk?.Map?.setCenter;
if (typeof getLL === "function" && typeof setC === "function") {
const mapEl = document.querySelector("#map") || document.querySelector(".olMap") || null;
const r = mapEl ? mapEl.getBoundingClientRect() : { left: 0, top: 0, width: window.innerWidth, height: window.innerHeight };
const cx = r.left + (r.width / 2);
const cy = r.top + (r.height / 2);
const x = Math.round(cx + dx);
const y = Math.round(cy + dy);
const ll = getLL({ x, y });
if (ll && isFinite(ll.lon) && isFinite(ll.lat)) {
setC({ lon: ll.lon, lat: ll.lat });
return true;
}
}
} catch {}
return false;
}
function getSegmentEndpoints(segId) {
const coords = segmentCoords(segId);
if (!coords.length) return null;
const a = coords[0];
const b = coords[coords.length - 1];
return { a: { lon: a[0], lat: a[1] }, b: { lon: b[0], lat: b[1] } };
}
async function goToEndpoint(segId, which) {
const ep = getSegmentEndpoints(segId);
if (!ep) { toast("Segment not loaded (zoom in to load)."); return; }
const p = which === "A" ? ep.a : ep.b;
const ok = zoomToLonLat(p.lon, p.lat);
toast(ok ? `Moved to ${which} node` : "Move failed (API mismatch).");
closeAllMenus();
}
function buildEndpointsSubmenu(segIds) {
if (segIds.length !== 1) return [];
return [
{ label: "Go to node A", sub: "Start of segment", onClick: () => goToEndpoint(segIds[0], "A") },
{ label: "Go to node B", sub: "End of segment", onClick: () => goToEndpoint(segIds[0], "B") },
];
}
async function actionCopySegmentIds(segIds) {
const text = segIds.join(", ");
await setClipboard(text);
toast(`Copied ID(s): ${text}`);
closeAllMenus();
}
async function actionCopyStreetName(firstSegId) {
const seg = sdkSegGetById(firstSegId);
if (!seg) { toast("Segment not found (zoom in until it loads)."); closeAllMenus(); return; }
const streetId = seg.primaryStreetId;
if (streetId == null) { await setClipboard("(no street)"); toast("Copied: (no street)"); closeAllMenus(); return; }
const street = sdkStreetsGetById(streetId);
const name = street?.streetName ?? street?.name ?? street?.englishName ?? "(unknown street)";
await setClipboard(String(name));
toast(`Copied street: ${name}`);
closeAllMenus();
}
function lonLatFromClick(clientX, clientY) {
try {
const fn = sdk?.Map?.getLonLatFromPixel;
if (typeof fn === "function") {
const ll = fn({ x: clientX, y: clientY });
if (ll && isFinite(ll.lon) && isFinite(ll.lat)) return ll;
}
} catch {}
return lastLonLat;
}
function gmapsUrlFromLonLat(ll) {
return `${GMAPS_BASE}${fmt(ll.lat)},${fmt(ll.lon)}`;
}
function osmUrlFromLonLat(ll, zoom = 19) {
const z = Number.isFinite(zoom) ? zoom : 19;
return `https://www.openstreetmap.org/?mlat=${fmt(ll.lat)}&mlon=${fmt(ll.lon)}#map=${z}/${fmt(ll.lat)}/${fmt(ll.lon)}`;
}
function wazeLiveMapUrlFromLonLat(ll, zoom = 17) {
const z = Number.isFinite(zoom) ? zoom : 17;
return `https://www.waze.com/live-map?zoom=${z}&lat=${fmt(ll.lat)}&lon=${fmt(ll.lon)}`;
}
function getZoomLevelBestEffort() {
try {
if (typeof sdk?.Map?.getZoom === "function") {
const z = sdk.Map.getZoom();
if (Number.isFinite(z)) return z;
}
} catch {}
try {
const z = UW?.W?.map?.getZoom?.();
if (Number.isFinite(z)) return z;
} catch {}
return null;
}
// ===== Permalink settings (used by Copy permalink / Refresh here) =====
const PERMALINK_SETTINGS_KEY = "wme_rc_permalink_settings_v1";
const DEFAULT_PERMALINK_SETTINGS = { zoomLocked: false, zoomLevel: null, includeLayers: true };
function loadPermalinkSettings() {
try {
const raw = localStorage.getItem(PERMALINK_SETTINGS_KEY);
if (!raw) return { ...DEFAULT_PERMALINK_SETTINGS };
const j = JSON.parse(raw);
const out = { ...DEFAULT_PERMALINK_SETTINGS, ...(j || {}) };
if (!Number.isFinite(out.zoomLevel)) out.zoomLevel = null;
out.zoomLocked = !!out.zoomLocked;
out.includeLayers = !!out.includeLayers;
return out;
} catch {
return { ...DEFAULT_PERMALINK_SETTINGS };
}
}
function savePermalinkSettings(s) {
try { localStorage.setItem(PERMALINK_SETTINGS_KEY, JSON.stringify(s || {})); } catch {}
}
function getLayerParamsFromCurrentURL() {
try {
const cur = new URLSearchParams(location.search);
const keepKeys = ["layers", "layer", "layersVisibility"];
const out = {};
for (const k of keepKeys) {
if (cur.has(k)) out[k] = cur.get(k);
}
return out;
} catch {
return {};
}
}
function showPermalinkSettingsModal() {
const st = loadPermalinkSettings();
const layerParams = getLayerParamsFromCurrentURL();
const layerText = Object.keys(layerParams).length
? Object.entries(layerParams).map(([k, v]) => `${k}=${v}`).join(" • ")
: "(no layer params found in URL)";
openModal({
title: "Permalink settings",
iconSvg: ICONS.chain,
bodyBuilder: ({ body, close }) => {
const wrap = document.createElement("div");
wrap.style.display = "flex";
wrap.style.flexDirection = "column";
wrap.style.gap = "12px";
const hint = document.createElement("div");
hint.className = "wmeRcHint";
hint.textContent = "Applies to Copy permalink and Refresh here.";
wrap.appendChild(hint);
// Zoom lock toggle + input
const zoomRow = document.createElement("div");
zoomRow.className = "wmeRcRow";
zoomRow.style.alignItems = "center";
zoomRow.innerHTML = `
<div class="wmeRcRowLabel" style="min-width:120px;">Zoom level</div>
<div style="display:flex;align-items:center;gap:10px;flex:1 1 auto;min-width:0;">
<label class="wmeRcInlineToggle" style="margin:0;">
<input type="checkbox" class="wmeRcZoomLock">
<span class="wmeRcSwitchTrack"><span class="wmeRcSwitchThumb"></span></span>
<span class="wmeRcSwitchLabel">Lock</span>
</label>
<input class="wmeRcInput wmeRcZoomInp" type="number" min="1" max="22" step="1" placeholder="Current" style="max-width:140px;">
</div>
`;
const zoomLock = zoomRow.querySelector(".wmeRcZoomLock");
const zoomInp = zoomRow.querySelector(".wmeRcZoomInp");
zoomLock.checked = !!st.zoomLocked;
zoomInp.value = (Number.isFinite(st.zoomLevel) ? String(st.zoomLevel) : "");
zoomInp.disabled = !zoomLock.checked;
zoomLock.addEventListener("change", () => {
zoomInp.disabled = !zoomLock.checked;
if (!zoomLock.checked) {
zoomInp.value = "";
zoomInp.classList.remove("bad");
}
});
zoomInp.addEventListener("input", () => {
zoomInp.classList.remove("bad");
});
wrap.appendChild(zoomRow);
// Include layers toggle
const layersWrap = document.createElement("div");
layersWrap.className = "wmeRcHint";
layersWrap.innerHTML = `
<label class="wmeRcInlineToggle" style="margin:0;">
<input type="checkbox" class="wmeRcLayersChk">
<span class="wmeRcSwitchTrack"><span class="wmeRcSwitchThumb"></span></span>
<span class="wmeRcSwitchLabel">Include layer settings</span>
</label>
<div style="margin-top:8px;opacity:.72;font-size:12px;line-height:1.25;word-break:break-word;">${escapeHtml(layerText)}</div>
`;
const layersChk = layersWrap.querySelector(".wmeRcLayersChk");
layersChk.checked = !!st.includeLayers;
wrap.appendChild(layersWrap);
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const btnCancel = document.createElement("div");
btnCancel.className = "wmeRcModalBtn";
btnCancel.textContent = "Cancel";
btnCancel.addEventListener("click", () => close());
const btnSave = document.createElement("div");
btnSave.className = "wmeRcModalBtn primary";
btnSave.textContent = "Save";
btnSave.addEventListener("click", () => {
const next = { ...DEFAULT_PERMALINK_SETTINGS };
next.includeLayers = !!layersChk.checked;
next.zoomLocked = !!zoomLock.checked;
if (next.zoomLocked) {
const z = Number(String(zoomInp.value || "").trim());
if (!Number.isFinite(z) || z < 1 || z > 22) {
zoomInp.classList.add("bad");
toast("Zoom level must be 1–22");
return;
}
next.zoomLevel = Math.round(z);
} else {
next.zoomLevel = null;
}
savePermalinkSettings(next);
toast("Saved: permalink settings");
close();
});
actions.appendChild(btnCancel);
actions.appendChild(btnSave);
wrap.appendChild(actions);
body.appendChild(wrap);
},
});
}
function buildPermalink(ll, segIds = null) {
const st = loadPermalinkSettings();
const z = (st && st.zoomLocked && Number.isFinite(st.zoomLevel)) ? Number(st.zoomLevel) : getZoomLevelBestEffort();
const cur = new URLSearchParams(location.search);
const params = new URLSearchParams();
for (const k of ["env", "tab", "language", "locale", "country"]) {
if (cur.has(k)) params.set(k, cur.get(k));
}
// Optional: include current layer params (if present)
if (st && st.includeLayers) {
for (const [k, v] of Object.entries(getLayerParamsFromCurrentURL())) {
if (v != null) params.set(k, String(v));
}
}
params.set("lat", fmt(ll.lat));
params.set("lon", fmt(ll.lon));
if (Number.isFinite(z)) params.set("zoomLevel", String(Math.round(z)));
if (Array.isArray(segIds) && segIds.length) params.set("segments", segIds.join(","));
const qs = params.toString();
return qs ? `${EDITOR_BASE}?${qs}` : EDITOR_BASE;
}
async function copyCoordsLatLon(ll) {
await setClipboard(`${fmt(ll.lat)}, ${fmt(ll.lon)}`);
toast("Copied: lat,lon");
closeAllMenus();
}
async function copyCoordsGmaps(ll) {
await setClipboard(gmapsUrlFromLonLat(ll));
toast("Copied: Google Maps link");
closeAllMenus();
}
async function copyPermalink(ll, segIds = null) {
const url = buildPermalink(ll, segIds);
await setClipboard(url);
toast("Copied: permalink");
closeAllMenus();
}
function refreshHere(ll, segIds = null) {
try {
if (!ll || !Number.isFinite(ll.lat) || !Number.isFinite(ll.lon)) {
toast("No map position yet. Move mouse on map first.");
closeAllMenus();
return;
}
const url = buildPermalink(ll, segIds);
closeAllMenus();
location.replace(url);
} catch (e) {
try { closeAllMenus(); location.reload(); } catch {}
}
}
function parseNumbersFromString(s) {
return (s.match(/-?\d+(\.\d+)?/g) || []).map(Number).filter(n => Number.isFinite(n));
}
function tryParseCoords(input) {
const nums = parseNumbersFromString(input);
if (nums.length < 2) return null;
const a = nums[0], b = nums[1];
const aIsLat = Math.abs(a) <= 90, bIsLon = Math.abs(b) <= 180;
const aIsLon = Math.abs(a) <= 180, bIsLat = Math.abs(b) <= 90;
if (aIsLat && bIsLon) return { lat: a, lon: b };
if (aIsLon && bIsLat) return { lat: b, lon: a };
return null;
}
function tryParseGoogleMaps(input) {
try {
const s = String(input || "").trim();
if (!s) return null;
// Fast check for common Google Maps hosts/paths
const looksLikeGmaps = /google\.[^\s/]+\/maps|maps\.google\.|\/maps\b|maps\.app\.goo\.gl|goo\.gl\/maps/i.test(s);
if (!looksLikeGmaps) return null;
// Pattern: .../@lat,lon,17z
const at = s.match(/@\s*(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)(?:,\s*(\d+(?:\.\d+)?)z)?/i);
if (at) {
const lat = Number(at[1]);
const lon = Number(at[2]);
if (Number.isFinite(lat) && Number.isFinite(lon)) return { lat, lon, zoom: null };
}
// Query params: q=lat,lon or ll=lat,lon
let url = null;
try { url = new URL(s); } catch { url = null; }
if (url) {
const q = url.searchParams.get("q") || url.searchParams.get("query") || url.searchParams.get("ll");
if (q) {
const cc = tryParseCoords(q);
if (cc) return { lat: cc.lat, lon: cc.lon, zoom: null };
}
}
// Some links embed coordinates in the path even without @
const cc2 = tryParseCoords(s);
if (cc2) return { lat: cc2.lat, lon: cc2.lon, zoom: null };
// Short links can't be resolved offline (needs a network redirect)
return { needsResolve: true };
} catch {
return null;
}
}
function tryParsePermalink(input) {
if (!/lat=|lon=|zoomLevel=|segments=/i.test(input)) return null;
const lat = (() => {
const m = input.match(/(?:\?|&)lat=([-0-9.]+)/i);
return m ? Number(m[1]) : null;
})();
const lon = (() => {
const m = input.match(/(?:\?|&)lon=([-0-9.]+)/i);
return m ? Number(m[1]) : null;
})();
const zoom = (() => {
const m = input.match(/(?:\?|&)zoomLevel=([0-9]+)/i);
return m ? Number(m[1]) : null;
})();
if (!Number.isFinite(lat) || !Number.isFinite(lon)) return null;
return { lat, lon, zoom: Number.isFinite(zoom) ? zoom : null };
}
// -------------------------------
// Jump / Search helpers (segment lookup + custom jump pin)
// -------------------------------
let __rcJumpPinLayer = null;
let __rcJumpPinMarker = null;
let __rcJumpPinTimer = null;
function __rcEnsureJumpPinLayer() {
try {
const map = getOlMapBestEffort();
const ol = UW?.OpenLayers;
if (!map || !ol) return null;
if (__rcJumpPinLayer && typeof map.getLayer === "function") {
// keep existing
return __rcJumpPinLayer;
}
// Marker layer (topmost)
const layer = new ol.Layer.Markers(`${SCRIPT_ID}:jumpPin`, { displayInLayerSwitcher: false });
try { layer.setZIndex?.(99999); } catch {}
try { map.addLayer(layer); } catch {}
__rcJumpPinLayer = layer;
return layer;
} catch {
return null;
}
}
function __rcMakeJumpPinSvg(label) {
const safe = String(label ?? "J").slice(0, 6).toUpperCase();
const svg = `
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="72" viewBox="0 0 72 72">
<defs>
<filter id="s" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="2" stdDeviation="2" flood-color="#000" flood-opacity="0.35"/>
</filter>
</defs>
<path filter="url(#s)" d="M36 3C23.3 3 13 13.3 13 26c0 18 23 43 23 43s23-25 23-43C59 13.3 48.7 3 36 3z"
fill="#fff" stroke="#111" stroke-width="3" />
<circle cx="36" cy="26" r="15" fill="#fff" stroke="#111" stroke-width="3"/>
<text x="36" y="31" text-anchor="middle"
font-family="Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif"
font-size="14" font-weight="900" fill="#111">${safe}</text>
</svg>
`.trim();
return "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
}
function __rcShowJumpPin(lon, lat, label) {
try {
const layer = __rcEnsureJumpPinLayer();
const map = getOlMapBestEffort();
const ol = UW?.OpenLayers;
if (!layer || !map || !ol) return;
// remove previous
try { if (__rcJumpPinMarker) layer.removeMarker(__rcJumpPinMarker); } catch {}
__rcJumpPinMarker = null;
try { if (__rcJumpPinTimer) clearTimeout(__rcJumpPinTimer); } catch {}
__rcJumpPinTimer = null;
// build marker
const ll = new ol.LonLat(Number(lon), Number(lat));
const iconUrl = __rcMakeJumpPinSvg(label);
const size = new ol.Size(72, 72);
const offset = new ol.Pixel(-36, -72);
const icon = new ol.Icon(iconUrl, size, offset);
const mk = new ol.Marker(ll, icon);
layer.addMarker(mk);
__rcJumpPinMarker = mk;
__rcJumpPinTimer = setTimeout(() => {
try {
if (__rcJumpPinLayer && __rcJumpPinMarker) {
__rcJumpPinLayer.removeMarker(__rcJumpPinMarker);
}
} catch {}
__rcJumpPinMarker = null;
__rcJumpPinTimer = null;
}, 6500);
} catch {}
}
async function __rcWaitFeaturesLoaded() {
let count = 0;
return new Promise((resolve) => {
const interval = setInterval(() => {
count++;
const loading = !!(UW?.W?.app?.layout?.model?.attributes?.loadingFeatures || UW?.W?.app?.layout?.model?.get?.("loadingFeatures") || UW?.W?.app?.layout?.model?.get?.("loading") === true);
if (!loading) {
clearInterval(interval);
resolve(true);
return;
}
if (count > 120) {
clearInterval(interval);
resolve(false);
}
}, 100);
});
}
function __rcParseSegmentIdList(input) {
const s = String(input || "").trim();
if (!s) return null;
if (!/^\d+(?:\s*,\s*\d+)*$/.test(s)) return null;
const ids = s.split(",").map(x => Number(String(x).trim())).filter(n => Number.isFinite(n) && n > 0);
if (!ids.length) return null;
return Array.from(new Set(ids));
}
async function __rcFindSegmentLonLat(segmentId) {
const id = Number(segmentId);
if (!Number.isFinite(id) || id <= 0) return null;
// 1) If already loaded, use its geometry center immediately
try {
const seg = sdkSegGetById(id);
const geom = seg?.geometry || seg?.geojson || null;
if (geom) {
const c = geoCenterFromAny(geom);
if (c && Number.isFinite(c.lon) && Number.isFinite(c.lat)) return { lon: c.lon, lat: c.lat, source: "loaded" };
}
} catch {}
// 2) Try WazeWrap SegmentFinder (works even when not loaded)
try {
const ww = UW?.WazeWrap || unsafeWindow?.WazeWrap || (typeof WazeWrap !== "undefined" ? WazeWrap : null);
if (ww?.Util?.findSegment) {
// some builds want (regionCode, segId); if regionCode is omitted, it still works in most cases
let res = null;
try { res = await ww.Util.findSegment(safeRegionCode(), String(id)); } catch {}
if (!res) {
try { res = await ww.Util.findSegment(safeRegionCode(), id); } catch {}
}
if (!res) {
try { res = await ww.Util.findSegment(null, id); } catch {}
}
if (res && Number.isFinite(Number(res.x)) && Number.isFinite(Number(res.y))) {
return { lon: Number(res.x), lat: Number(res.y), source: "wazewrap" };
}
}
} catch {}
// 3) Try SegmentFinder directly (w-tools) via GM_xmlhttpRequest
try {
if (typeof GM_xmlhttpRequest === "function") {
const url = `https://w-tools.org/api/SegmentFinder?find=${encodeURIComponent(String(id))}`;
const res = await new Promise((resolve) => {
GM_xmlhttpRequest({
method: "GET",
url,
onload: (r) => resolve(r),
onerror: () => resolve(null),
ontimeout: () => resolve(null),
timeout: 8000,
});
});
if (res && (res.status === 200 || res.status === 0) && res.responseText) {
let j = null;
try { j = JSON.parse(res.responseText); } catch {}
if (j && Number.isFinite(Number(j.x)) && Number.isFinite(Number(j.y))) {
return { lon: Number(j.x), lat: Number(j.y), source: "segmentfinder" };
}
}
}
} catch {}
// 3) Last resort: if SDK has findSegment, try it (API varies)
try {
if (sdk?.Segments?.findSegment) {
const out = await sdk.Segments.findSegment({ segmentId: id });
// best-effort parse for returned geometry/center
const geom = out?.geometry || out?.geojson || out?.segment?.geometry || null;
const c = geom ? geoCenterFromAny(geom) : null;
if (c && Number.isFinite(c.lon) && Number.isFinite(c.lat)) return { lon: c.lon, lat: c.lat, source: "sdk" };
}
} catch {}
return null;
}
function safeRegionCode() {
// Best-effort; if unknown, WazeWrap often still succeeds.
try {
const href = String(location.href || "");
const m = href.match(/\/\/[^/]+\/([a-z]{2,3})\/editor/i);
if (m && m[1]) return m[1].toUpperCase();
} catch {}
try {
const u = UW?.W?.app?.getAppRegion?.();
if (u) return String(u).toUpperCase();
} catch {}
return null;
}
function geoCenterFromAny(geom) {
try {
// SDK often returns GeoJSON-like { type, coordinates }
const gj = geom?.type && geom?.coordinates ? geom : (geom?.geometry || null);
const coords = gj?.coordinates;
if (!coords) return null;
// LineString -> average points
if (gj.type === "LineString" && Array.isArray(coords) && coords.length) {
let minLon = Infinity, minLat = Infinity, maxLon = -Infinity, maxLat = -Infinity;
for (const p of coords) {
const lon = Number(p?.[0]); const lat = Number(p?.[1]);
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
minLon = Math.min(minLon, lon); maxLon = Math.max(maxLon, lon);
minLat = Math.min(minLat, lat); maxLat = Math.max(maxLat, lat);
}
if (minLon !== Infinity) return { lon: (minLon + maxLon) / 2, lat: (minLat + maxLat) / 2 };
}
// MultiLineString -> first line
if (gj.type === "MultiLineString" && Array.isArray(coords) && coords.length) {
const first = coords[0];
if (Array.isArray(first) && first.length) {
let minLon = Infinity, minLat = Infinity, maxLon = -Infinity, maxLat = -Infinity;
for (const p of first) {
const lon = Number(p?.[0]); const lat = Number(p?.[1]);
if (!Number.isFinite(lon) || !Number.isFinite(lat)) continue;
minLon = Math.min(minLon, lon); maxLon = Math.max(maxLon, lon);
minLat = Math.min(minLat, lat); maxLat = Math.max(maxLat, lat);
}
if (minLon !== Infinity) return { lon: (minLon + maxLon) / 2, lat: (minLat + maxLat) / 2 };
}
}
} catch {}
return null;
}
async function __rcJumpAndSelectSegments(segIds) {
const ids = Array.from(new Set((segIds || []).map(Number).filter(n => Number.isFinite(n) && n > 0)));
if (!ids.length) { toast("Invalid segment id."); return false; }
// If already loaded: zoom + select
const loaded = ids.filter(id => !!sdkSegGetById(id));
if (loaded.length) {
try { zoomToSegments(loaded); } catch {}
setSelectionToSegmentIdsSilent(loaded);
return true;
}
// Not loaded: locate first segment, jump, wait, then select
const firstId = ids[0];
const loc = await __rcFindSegmentLonLat(firstId);
if (!loc) { toast("Segment not found / not accessible."); return false; }
const label = ids.length > 1 ? `${ids.length}X` : String(firstId).slice(-4);
__rcShowJumpPin(loc.lon, loc.lat, label);
// Jump to the located position; z18 like Enhanced Search
try { await jumpToCoords(loc.lat, loc.lon, 18, label); } catch { await jumpToCoords(loc.lat, loc.lon, 18, label); }
// Wait for features to load
await __rcWaitFeaturesLoaded();
// Now try selecting
const nowLoaded = ids.filter(id => !!sdkSegGetById(id));
if (!nowLoaded.length) { toast("Segment still not loaded (try again)."); return false; }
try { zoomToSegments(nowLoaded); } catch {}
setSelectionToSegmentIdsSilent(nowLoaded);
toast(ids.length === 1 ? `Jumped to segment ${firstId}` : `Jumped to ${nowLoaded.length}/${ids.length} segments`);
return true;
}
async function jumpToCoords(lat, lon, zoomMaybe, label) {
try {
if (sdk?.Map?.setCenter && typeof sdk.Map.setCenter === "function") {
sdk.Map.setCenter({ lon, lat });
} else if (sdk?.Map?.zoomToExtent) {
const d = 0.0025;
sdk.Map.zoomToExtent({ bbox: [lon - d, lat - d, lon + d, lat + d] });
}
if (Number.isFinite(zoomMaybe) && typeof sdk?.Map?.setZoom === "function") {
sdk.Map.setZoom({ zoomLevel: zoomMaybe });
} try { __rcShowJumpPin(lon, lat, (label || 'J')); } catch {}
toast(`Jumped to ${fmt(lat)}, ${fmt(lon)}`);
} catch (e) {
console.error(e);
toast("Jump failed (API mismatch).");
}
}
function showJumpModal(prefill = "") {
openModal({
title: "Jump / Search",
iconSvg: ICONS.jump,
bodyBuilder: ({ body, close }) => {
const inp = document.createElement("input");
inp.className = "wmeRcInput";
inp.placeholder = "Paste coords, permalink, Google Maps link, or segment ID…";
inp.value = prefill || "";
const hint = document.createElement("div");
hint.className = "wmeRcHint";
hint.innerHTML = `Examples: <span style="opacity:.92;">37.983, 23.728</span> • <span style="opacity:.92;">259312931</span> • <span style="opacity:.92;">WME permalink</span> • <span style="opacity:.92;">Google Maps link</span>`;
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const btnCancel = document.createElement("div");
btnCancel.className = "wmeRcModalBtn";
btnCancel.textContent = "Cancel";
btnCancel.addEventListener("click", () => { try { if (typeof cleanupSoundPick === "function") cleanupSoundPick(); } catch {} close(); });
const btnGo = document.createElement("div");
btnGo.className = "wmeRcModalBtn primary";
btnGo.textContent = "Go";
const run = async () => {
const input = (inp.value || "").trim();
if (!input) { toast("Paste something."); return; }
const gm = tryParseGoogleMaps(input);
if (gm && gm.needsResolve) { toast("Google Maps short link can’t be parsed here. Open it and copy the full link with coordinates."); return; }
if (gm && Number.isFinite(gm.lat) && Number.isFinite(gm.lon)) { await jumpToCoords(gm.lat, gm.lon, gm.zoom); close(); return; }
const pm = tryParsePermalink(input);
if (pm) { await jumpToCoords(pm.lat, pm.lon, pm.zoom); close(); return; }
const cc = tryParseCoords(input);
if (cc) { await jumpToCoords(cc.lat, cc.lon, null); close(); return; }
const segIds = __rcParseSegmentIdList(input);
if (segIds) {
await __rcJumpAndSelectSegments(segIds);
close();
return;
}
toast("Couldn’t parse input (coords/permalink/google maps/segment id).");
};
btnGo.addEventListener("click", run);
inp.addEventListener("keydown", (e) => {
if (e.key === "Enter") { e.preventDefault(); run(); }
});
actions.appendChild(btnCancel);
actions.appendChild(btnGo);
body.appendChild(inp);
body.appendChild(actions);
requestAnimationFrame(() => {
requestAnimationFrame(() => {
try { inp.focus(); inp.select(); } catch {}
});
});
}
});
}
function pickAttributesFromSegmentId(segmentId) {
const seg = sdkSegGetById(segmentId);
const out = {};
if (Number.isFinite(seg?.lockRank)) out.lockRank = seg.lockRank;
out.fwdSpeedLimit = (seg?.fwdSpeedLimit == null ? null : Number(seg.fwdSpeedLimit));
out.revSpeedLimit = (seg?.revSpeedLimit == null ? null : Number(seg.revSpeedLimit));
if (typeof seg?.isAtoB === "boolean") out.isAtoB = seg.isAtoB;
if (typeof seg?.isBtoA === "boolean") out.isBtoA = seg.isBtoA;
const lvl = getSegLevelById(segmentId);
if (Number.isFinite(lvl)) out.level = lvl;
return out;
}
function pasteCfgSummary() {
const on = [];
if (pasteCfg.speed) on.push("speed");
if (pasteCfg.lock) on.push("lock");
if (pasteCfg.direction) on.push("dir");
if (pasteCfg.elevation) on.push("elev");
return on.length ? on.join(", ") : "none";
}
function canPasteAnything() {
return !!(pasteCfg.speed || pasteCfg.lock || pasteCfg.direction || pasteCfg.elevation);
}
async function actionCopyAttributesFromSelection(segIds) {
const id = Number(segIds?.[0]);
if (!Number.isFinite(id)) { toast("Cannot copy attrs: no segment."); closeAllMenus(); return; }
const seg = sdkSegGetById(id);
const segW = UW?.W?.model?.segments?.getObjectById?.(id) || null;
if (!seg && !segW) { toast("Cannot copy attrs: segment not loaded."); closeAllMenus(); return; }
attrClip = pickAttributesFromSegmentId(id);
toast("Copied attributes");
closeAllMenus();
}
function sameValue(a, b) {
const na = (a == null ? null : a);
const nb = (b == null ? null : b);
if (typeof na === "number" && typeof nb === "number") return Number(na) === Number(nb);
return na === nb;
}
function computePasteDiff(segIds) {
const diff = {
total: segIds.length,
missing: 0,
lock: { change: 0, same: 0, target: null, mixed: false, sampleCur: null },
speed: { change: 0, same: 0, target: null, mixed: false, sampleCur: null },
direction: { change: 0, same: 0, target: null, mixed: false, sampleCur: null },
elevation: { change: 0, same: 0, target: null, mixed: false, sampleCur: null },
overall: { change: 0, same: 0 }
};
const userLevel = getUserLevel() ?? 1;
const targetLockLevel = ("lockRank" in (attrClip || {})) ? lockRankToLevel(Number(attrClip.lockRank), userLevel) : null;
diff.lock.target = targetLockLevel != null ? `L${targetLockLevel}` : "—";
const tf = ("fwdSpeedLimit" in (attrClip || {})) ? (attrClip.fwdSpeedLimit == null ? "∅" : String(attrClip.fwdSpeedLimit)) : "—";
const tr = ("revSpeedLimit" in (attrClip || {})) ? (attrClip.revSpeedLimit == null ? "∅" : String(attrClip.revSpeedLimit)) : "—";
diff.speed.target = (tf === "—" && tr === "—") ? "—" : `${tf}/${tr}`;
const tdA = ("isAtoB" in (attrClip || {})) ? (!!attrClip.isAtoB ? "1" : "0") : "—";
const tdB = ("isBtoA" in (attrClip || {})) ? (!!attrClip.isBtoA ? "1" : "0") : "—";
diff.direction.target = (tdA === "—" && tdB === "—") ? "—" : `A→B:${tdA} B→A:${tdB}`;
diff.elevation.target = ("level" in (attrClip || {})) ? String(attrClip.level ?? "—") : "—";
let sampleLock = null, sampleSpeed = null, sampleDir = null, sampleElev = null;
for (const id of segIds) {
const seg = sdkSegGetById(id);
if (!seg) { diff.missing++; continue; }
const curLockLevel = (Number.isFinite(seg.lockRank) ? lockRankToLevel(Number(seg.lockRank), userLevel) : null);
const curSpeed = `${seg.fwdSpeedLimit == null ? "∅" : seg.fwdSpeedLimit}/${seg.revSpeedLimit == null ? "∅" : seg.revSpeedLimit}`;
const curDir = `A→B:${seg.isAtoB ? "1" : "0"} B→A:${seg.isBtoA ? "1" : "0"}`;
const lvlNow = getSegLevelById(id);
const curElev = (lvlNow == null ? "—" : String(lvlNow));
if (sampleLock == null) sampleLock = curLockLevel != null ? `L${curLockLevel}` : "—";
if (sampleSpeed == null) sampleSpeed = curSpeed;
if (sampleDir == null) sampleDir = curDir;
if (sampleElev == null) sampleElev = curElev;
const patch = { lock: false, speed: false, direction: false, elevation: false };
if ("lockRank" in (attrClip || {})) {
patch.lock = !sameValue(seg.lockRank, attrClip.lockRank);
}
if (("fwdSpeedLimit" in (attrClip || {})) || ("revSpeedLimit" in (attrClip || {}))) {
const cf = seg.fwdSpeedLimit == null ? null : Number(seg.fwdSpeedLimit);
const cr = seg.revSpeedLimit == null ? null : Number(seg.revSpeedLimit);
const tf2 = ("fwdSpeedLimit" in (attrClip || {})) ? (attrClip.fwdSpeedLimit == null ? null : Number(attrClip.fwdSpeedLimit)) : cf;
const tr2 = ("revSpeedLimit" in (attrClip || {})) ? (attrClip.revSpeedLimit == null ? null : Number(attrClip.revSpeedLimit)) : cr;
patch.speed = (!sameValue(cf, tf2)) || (!sameValue(cr, tr2));
}
if (("isAtoB" in (attrClip || {})) || ("isBtoA" in (attrClip || {}))) {
const ta = ("isAtoB" in (attrClip || {})) ? !!attrClip.isAtoB : !!seg.isAtoB;
const tb = ("isBtoA" in (attrClip || {})) ? !!attrClip.isBtoA : !!seg.isBtoA;
patch.direction = (!sameValue(!!seg.isAtoB, ta)) || (!sameValue(!!seg.isBtoA, tb));
}
if ("level" in (attrClip || {})) {
patch.elevation = !sameValue(getSegLevelById(id), attrClip.level);
}
const anyChangeIfSelected = (cfg) => {
return (cfg.lock && patch.lock) || (cfg.speed && patch.speed) || (cfg.direction && patch.direction) || (cfg.elevation && patch.elevation);
};
if (patch.lock) diff.lock.change++; else diff.lock.same++;
if (patch.speed) diff.speed.change++; else diff.speed.same++;
if (patch.direction) diff.direction.change++; else diff.direction.same++;
if (patch.elevation) diff.elevation.change++; else diff.elevation.same++;
if (anyChangeIfSelected(pasteCfg)) diff.overall.change++;
else diff.overall.same++;
}
diff.lock.sampleCur = sampleLock ?? "—";
diff.speed.sampleCur = sampleSpeed ?? "—";
diff.direction.sampleCur = sampleDir ?? "—";
diff.elevation.sampleCur = sampleElev ?? "—";
return diff;
}
async function actionPasteAttributesToSelection(segIds) {
if (!attrClip) { toast("No copied attributes yet."); closeAllMenus(); return; }
if (!canPasteAnything()) { toast("Paste selection is empty."); return; }
let changed = 0, unchanged = 0, fail = 0;
for (const rawId of segIds) {
const id = Number(rawId);
if (!Number.isFinite(id)) { fail++; continue; }
const seg = sdkSegGetById(id);
let segmentChanged = false;
let segmentFailed = false;
const patch = {};
if (seg) {
if (pasteCfg.lock && ("lockRank" in attrClip)) {
if (!sameValue(seg.lockRank, attrClip.lockRank)) patch.lockRank = attrClip.lockRank;
}
if (pasteCfg.speed) {
if ("fwdSpeedLimit" in attrClip) {
const cur = (seg.fwdSpeedLimit == null ? null : Number(seg.fwdSpeedLimit));
if (!sameValue(cur, attrClip.fwdSpeedLimit)) patch.fwdSpeedLimit = attrClip.fwdSpeedLimit;
}
if ("revSpeedLimit" in attrClip) {
const cur = (seg.revSpeedLimit == null ? null : Number(seg.revSpeedLimit));
if (!sameValue(cur, attrClip.revSpeedLimit)) patch.revSpeedLimit = attrClip.revSpeedLimit;
}
}
if (pasteCfg.direction) {
if ("isAtoB" in attrClip) {
if (!sameValue(!!seg.isAtoB, !!attrClip.isAtoB)) patch.isAtoB = !!attrClip.isAtoB;
}
if ("isBtoA" in attrClip) {
if (!sameValue(!!seg.isBtoA, !!attrClip.isBtoA)) patch.isBtoA = !!attrClip.isBtoA;
}
}
const keys = Object.keys(patch);
if (keys.length) {
try { sdkSegUpdateSegment(id, patch); segmentChanged = true; }
catch { segmentFailed = true; }
}
} else {
if (pasteCfg.lock || pasteCfg.speed || pasteCfg.direction) segmentFailed = true;
}
if (pasteCfg.elevation && ("level" in (attrClip || {}))) {
const curLvl = getSegLevelById(id);
if (!sameValue(curLvl, attrClip.level)) {
const ok = setSegLevelById(id, attrClip.level);
if (ok) segmentChanged = true;
else segmentFailed = true;
}
}
if (segmentFailed) fail++;
if (segmentChanged) changed++; else unchanged++;
}
if (changed === 0 && fail === 0) {
toast(`Paste Attributes: no changes (already identical) • ${unchanged}/${segIds.length}`);
} else {
toast(`Pasted (${pasteCfgSummary()}) • changed ${changed}, unchanged ${unchanged}${fail ? `, failed ${fail}` : ""}`);
}
closeAllMenus();
}
function tagHtml(kind, text) {
const cls = kind === "ok" ? "ok" : (kind === "bad" ? "bad" : "warn");
return `<span class="wmeRcTag ${cls}">${text}</span>`;
}
function showPasteSelectorModal(segIds) {
if (!attrClip) { toast("Copy attributes first."); return; }
openModal({
title: "Paste Attributes",
iconSvg: ICONS.gear,
bodyBuilder: ({ body, close }) => {
const hint = document.createElement("div");
hint.className = "wmeRcHint";
hint.textContent = "Select what will be pasted. Preview shows what will change for the current selection.";
const summary = document.createElement("div");
summary.className = "wmeRcHint";
const grid = document.createElement("div");
grid.className = "wmeRcToggleGrid";
const diffBox = document.createElement("div");
diffBox.className = "wmeRcDiffBox";
const actions = document.createElement("div");
actions.className = "wmeRcModalActions";
const refreshGrid = () => {
grid.innerHTML = "";
grid.appendChild(mkToggle("speed", "Speed limits", "Forward/reverse speed"));
grid.appendChild(mkToggle("lock", "Lock", "Lock rank"));
grid.appendChild(mkToggle("direction", "Direction", "A→B / B→A"));
grid.appendChild(mkToggle("elevation", "Elevation", "Elevation (level)"));
};
const mkToggle = (key, name, desc) => {
const on = !!pasteCfg[key];
const el = document.createElement("div");
el.className = "wmeRcToggleBtn " + (on ? "on" : "off");
el.innerHTML = `
<div class="wmeRcToggleLeft">
<div class="wmeRcToggleName">${name}</div>
<div class="wmeRcToggleDesc">${desc}</div>
</div>
<div class="wmeRcPill ${on ? "on" : "off"}">${on ? "ON" : "OFF"}</div>
`;
el.addEventListener("click", () => {
pasteCfg[key] = !pasteCfg[key];
refreshGrid();
refreshDiff();
updateSummary();
});
return el;
};
const refreshDiff = () => {
const d = computePasteDiff(segIds);
diffBox.innerHTML = "";
const overallKind = (!canPasteAnything()) ? "bad" : (d.overall.change > 0 ? "ok" : "warn");
const overallText = (!canPasteAnything()) ? "select something" : (d.overall.change > 0 ? `${d.overall.change} will change` : "no changes");
const overall = document.createElement("div");
overall.className = "wmeRcDiffRow";
overall.innerHTML = `
<div class="wmeRcDiffLeft">
<div class="wmeRcDiffName">Preview</div>
<div class="wmeRcDiffDesc">${d.total} selected${d.missing ? ` • ${d.missing} not loaded` : ""}</div>
</div>
<div class="wmeRcDiffRight">${tagHtml(overallKind, overallText)}</div>
`;
diffBox.appendChild(overall);
diffBox.appendChild(makeDiffRow("Lock", d.lock.sampleCur, d.lock.target, pasteCfg.lock, d.lock.change));
diffBox.appendChild(makeDiffRow("Speed", d.speed.sampleCur, d.speed.target, pasteCfg.speed, d.speed.change));
diffBox.appendChild(makeDiffRow("Direction", d.direction.sampleCur, d.direction.target, pasteCfg.direction, d.direction.change));
diffBox.appendChild(makeDiffRow("Elevation", d.elevation.sampleCur, d.elevation.target, pasteCfg.elevation, d.elevation.change));
};
const makeDiffRow = (name, cur, target, enabledNow, changeCount) => {
const row = document.createElement("div");
row.className = "wmeRcDiffRow";
const kind = !enabledNow ? "warn" : (changeCount > 0 ? "ok" : "warn");
const tag = !enabledNow ? tagHtml("warn", "disabled") : (changeCount > 0 ? tagHtml("ok", `${changeCount} change`) : tagHtml("warn", "no change"));
row.innerHTML = `
<div class="wmeRcDiffLeft">
<div class="wmeRcDiffName">${name}</div>
<div class="wmeRcDiffDesc">${enabledNow ? "will apply if different" : "not included"}</div>
</div>
<div class="wmeRcDiffRight">${cur} → ${target} ${tag}</div>
`;
return row;
};
const updateSummary = () => {
summary.textContent = `Selected: ${pasteCfgSummary()}`;
const ok = !!attrClip && segIds.length > 0 && canPasteAnything();
btnPasteNow.style.opacity = ok ? "1" : ".55";
btnPasteNow.style.pointerEvents = ok ? "auto" : "none";
};
const btnNone = document.createElement("div");
btnNone.className = "wmeRcModalBtn";
btnNone.textContent = "Select none";
btnNone.addEventListener("click", () => {
pasteCfg.speed = pasteCfg.lock = pasteCfg.direction = pasteCfg.elevation = false;
refreshGrid(); refreshDiff(); updateSummary();
});
const btnAll = document.createElement("div");
btnAll.className = "wmeRcModalBtn";
btnAll.textContent = "Select all";
btnAll.addEventListener("click", () => {
pasteCfg.speed = pasteCfg.lock = pasteCfg.direction = pasteCfg.elevation = true;
refreshGrid(); refreshDiff(); updateSummary();
});
const btnPasteNow = document.createElement("div");
btnPasteNow.className = "wmeRcModalBtn primary";
btnPasteNow.textContent = "Paste Now";
btnPasteNow.addEventListener("click", async () => {
const ok = !!attrClip && segIds.length > 0 && canPasteAnything();
if (!ok) return;
close();
await actionPasteAttributesToSelection(segIds);
});
const btnSave = document.createElement("div");
btnSave.className = "wmeRcModalBtn";
btnSave.textContent = "Save";
btnSave.addEventListener("click", () => { toast(`Paste set: ${pasteCfgSummary()}`); close(); });
actions.appendChild(btnNone);
actions.appendChild(btnAll);
actions.appendChild(btnSave);
actions.appendChild(btnPasteNow);
body.appendChild(summary);
body.appendChild(grid);
body.appendChild(diffBox);
body.appendChild(actions);
refreshGrid();
refreshDiff();
updateSummary();
},
});
}
function selectSameStreet(segIds) {
const seg0 = sdkSegGetById(segIds[0]);
const streetId = seg0?.primaryStreetId;
if (streetId == null) { toast("No primary street on this segment."); return; }
const loaded = getAllLoadedSegmentsW();
const matchIds = [];
for (const s of loaded) {
if (primaryStreetIdFromW(s) == streetId) {
const id = segIdFromW(s);
if (Number.isFinite(id)) matchIds.push(id);
}
}
if (!matchIds.length) { toast("No matching segments loaded."); return; }
setSelectionToSegmentIds(matchIds);
}
function selectSameLockInView(segIds) {
const info = getSegmentsLockInfo(segIds);
if (info.mixed || info.currentRaw == null) { toast("Selection lock is mixed/unknown."); return; }
const target = info.currentRaw;
const loaded = getAllLoadedSegmentsW();
const matchIds = [];
for (const s of loaded) {
const lk = lockRankFromW(s);
if (lk != null && lk === target) {
const id = segIdFromW(s);
if (Number.isFinite(id)) matchIds.push(id);
}
}
if (!matchIds.length) { toast("No matching lock segments loaded."); return; }
setSelectionToSegmentIds(matchIds);
}
function selectSameSpeedInView(segIds) {
const info = getSegmentsSpeedInfo(segIds);
if (info.mixed) { toast("Selection speed is mixed."); return; }
const seg0 = sdkSegGetById(segIds[0]);
if (!seg0) { toast("Segment not loaded."); return; }
const mode0 = getSegDirectionMode(seg0);
const f0 = seg0.fwdSpeedLimit == null ? null : Number(seg0.fwdSpeedLimit);
const r0 = seg0.revSpeedLimit == null ? null : Number(seg0.revSpeedLimit);
const loaded = getAllLoadedSegmentsW();
const matchIds = [];
for (const s of loaded) {
const id = segIdFromW(s);
if (!Number.isFinite(id)) continue;
const sp = speedFromW(s);
const d = dirFromW(s);
const isTwo = d.mode === "2";
const isA = d.mode === "A";
const isB = d.mode === "B";
if (mode0 === "BOTH") {
if (!isTwo) continue;
if ((sp.f ?? null) === (f0 ?? null) && (sp.r ?? null) === (r0 ?? null)) matchIds.push(id);
} else if (mode0 === "FWD") {
if (!isA) continue;
if ((sp.f ?? null) === (f0 ?? null)) matchIds.push(id);
} else if (mode0 === "REV") {
if (!isB) continue;
if ((sp.r ?? null) === (r0 ?? null)) matchIds.push(id);
}
}
if (!matchIds.length) { toast("No matching speed segments loaded."); return; }
setSelectionToSegmentIds(matchIds);
}
function nodeIdFromW(segW, which) {
const a = segW?.attributes || {};
return a[which + "NodeID"] ?? a[which + "NodeId"] ?? a[which + "Node"] ?? segW?.[which + "NodeID"] ?? null;
}
function connectedSegmentsAtNode(nodeId, loadedSegs) {
const out = [];
for (const s of loadedSegs) {
const fromN = nodeIdFromW(s, "from");
const toN = nodeIdFromW(s, "to");
if (fromN == nodeId || toN == nodeId) {
const id = segIdFromW(s);
if (Number.isFinite(id)) out.push({ seg: s, id });
}
}
return out;
}
function selectConnectedChain(segIds) {
const loaded = getAllLoadedSegmentsW();
if (!loaded.length) { toast("No segments loaded."); return; }
const all = new Set();
for (const startId of segIds) {
const startW = UW?.W?.model?.segments?.getObjectById?.(startId) || null;
if (!startW) { all.add(startId); continue; }
all.add(startId);
const fromNode = nodeIdFromW(startW, "from");
const toNode = nodeIdFromW(startW, "to");
const walk = (nodeId, comingSegId) => {
let currentNode = nodeId;
let prevSegId = comingSegId;
while (currentNode != null) {
const con = connectedSegmentsAtNode(currentNode, loaded)
.map(x => x.id)
.filter(id => id !== prevSegId);
if (con.length !== 1) break;
const nextSegId = con[0];
all.add(nextSegId);
const nextW = UW?.W?.model?.segments?.getObjectById?.(nextSegId) || null;
if (!nextW) break;
const nFrom = nodeIdFromW(nextW, "from");
const nTo = nodeIdFromW(nextW, "to");
const nextNode = (nFrom == currentNode) ? nTo : nFrom;
prevSegId = nextSegId;
currentNode = nextNode;
}
};
walk(fromNode, startId);
walk(toNode, startId);
}
const ids = Array.from(all).filter(Number.isFinite);
setSelectionToSegmentIds(ids);
}
function showSelectModal(segIds) {
const lockInfo = getSegmentsLockInfo(segIds);
const speedInfo = getSegmentsSpeedInfo(segIds);
openModal({
title: "Select…",
iconSvg: ICONS.select,
bodyBuilder: ({ body, close }) => {
const hint = document.createElement("div");
hint.className = "wmeRcHint";
hint.textContent = "Works on loaded segments only (current view / loaded tiles).";
const list = document.createElement("div");
list.className = "wmeRcActionList";
const add = (title, desc, icon, disabled, fn) => {
const card = document.createElement("div");
card.className = "wmeRcActionCard" + (disabled ? " disabled" : "");
card.innerHTML = `
<div class="wmeRcActionLeft">
<div class="wmeRcActionTitle"><span class="wmeRcI">${icon}</span><span>${title}</span></div>
<div class="wmeRcActionDesc">${desc}</div>
</div>
<div class="wmeRcChevron">›</div>
`;
if (!disabled) {
card.addEventListener("click", () => {
try { fn(); } catch (e) { console.error(e); toast(String(e?.message || e)); }
close();
});
}
list.appendChild(card);
};
add("Same street", "Primary street (in view)", ICONS.street, false, () => selectSameStreet(segIds));
add("Same lock", lockInfo.mixed ? "Disabled: selection lock is mixed" : "Same lock rank (in view)", ICONS.lock, lockInfo.mixed || lockInfo.currentRaw == null, () => selectSameLockInView(segIds));
body.appendChild(list);
}
});
}
function roadTypeNameBestEffort(roadType) {
const rt = (roadType == null ? null : Number(roadType));
if (!Number.isFinite(rt)) return null;
try {
const rtObj = UW?.W?.model?.roadTypes?.getObjectById?.(rt) || null;
const name = rtObj?.name || rtObj?.title || rtObj?.localizedName || null;
if (name) return String(name);
} catch {}
return null;
}
function getStreetAndCityForSegmentId(segId) {
let streetName = null;
let cityName = null;
try {
const seg = sdkSegGetById(segId);
const streetId = seg?.primaryStreetId;
if (streetId != null) {
const street = sdkStreetsGetById(streetId);
streetName = street?.streetName ?? street?.name ?? street?.englishName ?? null;
cityName = street?.cityName ?? null;
const cityId = street?.cityId ?? street?.cityID ?? null;
if (!cityName && cityId != null) {
try {
const c = UW?.W?.model?.cities?.getObjectById?.(Number(cityId));
cityName = c?.name ?? null;
} catch {}
}
}
} catch {}
if (!streetName) {
try {
streetName = getStreetNameForSegmentId(segId);
if (streetName && String(streetName).startsWith("(")) streetName = null;
} catch {}
}
if (!cityName) {
try {
const segW = segWGetById(segId);
const streetIdW = segW?.primaryStreetID ?? segW?.primaryStreetId ?? segW?.attributes?.primaryStreetID ?? segW?.attributes?.primaryStreetId ?? null;
if (streetIdW != null) {
const st = UW?.W?.model?.streets?.getObjectById?.(Number(streetIdW));
if (!streetName) streetName = st?.name ?? null;
const cId = st?.cityID ?? st?.cityId ?? st?.attributes?.cityID ?? st?.attributes?.cityId ?? null;
if (cId != null) {
const c = UW?.W?.model?.cities?.getObjectById?.(Number(cId));
cityName = c?.name ?? null;
}
}
} catch {}
}
return {
street: streetName ? String(streetName) : "(unknown)",
city: cityName ? String(cityName) : "(unknown)",
};
}
function summarizeValues(values) {
const clean = (values || []).map(v => (v == null ? "(unknown)" : String(v)));
const counts = new Map();
for (const v of clean) counts.set(v, (counts.get(v) || 0) + 1);
const uniq = Array.from(counts.keys());
const mixed = uniq.length > 1;
if (!mixed) return { mixed: false, text: uniq[0] || "(unknown)", detail: "" };
const top = Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 4)
.map(([k, c]) => `${k} (${c})`);
const more = counts.size > 4 ? ` +${counts.size - 4} more` : "";
return { mixed: true, text: `mixed (${counts.size})`, detail: top.join(" • ") + more };
}
function buildToolsSubmenuForSegments(segIds, ctxLL) {
const ll = ctxLL || lastLonLat;
return [
{ label: withIcon(ICONS.select, "Select…"), sub: "Smart selection tools", onClick: () => showSelectModal(segIds) },
{ type: "sep" },
{ label: withIcon(ICONS.copy, "Copy attributes"), sub: "speed • lock • direction • elev", onClick: () => actionCopyAttributesFromSelection(segIds) },
{
label: withIcon(ICONS.tools, "Paste Attributes"),
sub: attrClip ? `Selected: ${pasteCfgSummary()}` : "Nothing copied yet",
disabled: !attrClip,
iconButton: { html: `<span class="wmeRcI">${ICONS.gear}</span>`, title: "Choose what to paste", onClick: () => showPasteSelectorModal(segIds) },
onClick: () => actionPasteAttributesToSelection(segIds),
},
];
}
function getStreetNameForSegmentId(segId) {
const seg = sdkSegGetById(segId);
if (!seg) return "(unknown street)";
const streetId = seg.primaryStreetId;
if (streetId == null) return "(no street)";
const street = sdkStreetsGetById(streetId);
const name = street?.streetName ?? street?.name ?? street?.englishName ?? "(unknown street)";
return String(name);
}
async function actionCopySelectionSummary(segIds, ll) {
const idsTxt = segIds.join(", ");
const streetTxt = segIds.length ? getStreetNameForSegmentId(segIds[0]) : "(no segment)";
const pm = buildPermalink(ll, segIds);
const gmaps = gmapsUrlFromLonLat(ll);
const out = `Street: ${streetTxt}\nSegment ID(s): ${idsTxt}\nCoords: ${fmt(ll.lat)}, ${fmt(ll.lon)}\nGoogle Maps: ${gmaps}\nPermalink: ${pm}`;
await setClipboard(out);
toast("Copied: selection summary");
closeAllMenus();
}
function buildCopySubmenuForSegments(segIds, ctxLL) {
const ll = ctxLL || lastLonLat;
const hasLL = !!(ll && Number.isFinite(ll.lat) && Number.isFinite(ll.lon));
return [
{ label: withIcon(ICONS.street, "Street name"), sub: "From first selected", onClick: () => actionCopyStreetName(segIds[0]) },
{ label: withIcon(ICONS.copy, "Segment ID"), sub: segIds.length === 1 ? String(segIds[0]) : `${segIds.length} segments`, onClick: () => actionCopySegmentIds(segIds) },
{ type: "sep" },
{ label: withIcon(ICONS.ext, "Google Maps link"), sub: hasLL ? "https://www.google.com/…" : "Right-click on map", disabled: !hasLL, onClick: () => copyCoordsGmaps(ll), rightButton: hasLL ? { label: "Open", onClick: () => { try { UW.open(gmapsUrlFromLonLat(ll), "_blank", "noopener,noreferrer"); } catch (e) { console.error(e); } closeAllMenus(); } } : null },
{ type: "sep" },
{ label: withIcon(ICONS.copy, "Summary"), sub: hasLL ? "Street • IDs • Links" : "Right-click on map", disabled: !hasLL, onClick: () => actionCopySelectionSummary(segIds, ll) },
];
}
function openSegmentMenu(x, y, segIds, ctxLL) {
try { menuState.rootLL = (ctxLL || lastLonLat || null); menuState.rootType = "segment"; } catch {}
const dirInfo = dirSummaryForSelection(segIds);
const dirSub = dirInfo.mixed ? "mixed" : modeLabel(dirInfo.mode);
const ll = ctxLL || lastLonLat;
const hasLL = !!(ll && Number.isFinite(ll.lat) && Number.isFinite(ll.lon));
openRootMenu(x, y, "Segment", `${segIds.length} selected`, [
{ label: withIcon(ICONS.copy, "Copy coordinates"), sub: hasLL ? `${fmt(ll.lat)}, ${fmt(ll.lon)}` : "Move mouse on map first", disabled: !hasLL, onClick: () => copyCoordsLatLon(ll) },
{ label: withIcon(ICONS.chain, "Copy permalink"), sub: hasLL ? "WME permalink" : "Move mouse on map first", disabled: !hasLL, onClick: () => copyPermalink(ll, segIds) },
{ label: withIcon(ICONS.refresh, "Refresh here"), sub: hasLL ? "Reload editor at this spot" : "Move mouse on map first", disabled: !hasLL, onClick: () => refreshHere(ll, segIds) },
{ label: withIcon(ICONS.mapPin, "Pin this Place"), sub: hasLL ? "Save in pins panel" : "Move mouse on map first", disabled: !hasLL, onClick: () => pinThisPlace("segment", ll, segIds) },
{ type: "sep" },
{ label: withIcon(ICONS.endpoints, "Endpoints"), sub: segIds.length === 1 ? "Go to A/B node" : "Select 1 segment", disabled: segIds.length !== 1, submenu: true, submenuKind: "endpoints", submenuHeaderLeft: "Endpoints", submenuHeaderRight: "", getSubmenuItems: () => buildEndpointsSubmenu(segIds) },
{ type: "sep" },
{ label: withIcon(ICONS.jump, "Jump / Search…"), sub: "coords • permalink • google maps • segment id", onClick: () => showJumpModal("") },
{ label: withIcon(ICONS.more, "More"), sub: "Tools & utilities", submenu: true, submenuKind: "tools", submenuHeaderLeft: "Tools", submenuHeaderRight: "", getSubmenuItems: () => buildToolsSubmenuForSegments(segIds, ll) },
]);
}
function openMapMenu(x, y, ll) {
const has = !!ll;
try { menuState.rootLL = (ll || lastLonLat || null); menuState.rootType = "map"; } catch {}
openRootMenu(x, y, "Map", "No selection", [
{ label: withIcon(ICONS.copy, "Copy coordinates"), sub: has ? `${fmt(ll.lat)}, ${fmt(ll.lon)}` : "Move mouse on map first", disabled: !has, onClick: () => copyCoordsLatLon(ll) },
{ label: withIcon(ICONS.mapPin, "Pin this Place"), sub: has ? "Save in pins panel" : "Move mouse on map first", disabled: !has, onClick: () => pinThisPlace("map", ll, null) },
{ label: withIcon(ICONS.refresh, "Refresh here"), sub: has ? "Reload editor at this spot" : "Move mouse on map first", disabled: !has, onClick: () => refreshHere(ll, null) },
{ label: withIcon(ICONS.chain, "Copy permalink"), sub: has ? "WME permalink (lat/lon/zoom)" : "Move mouse on map first", disabled: !has, onClick: () => copyPermalink(ll, null) },
{ label: withIcon(ICONS.ext, "Open in Google Maps"), sub: has ? "https://www.google.com/…" : "Move mouse on map first", disabled: !has,
onClick: () => { try { UW.open(gmapsUrlFromLonLat(ll), "_blank", "noopener,noreferrer"); } catch (e) { console.error(e); } closeAllMenus(); },
rightButton: has ? { label: "Copy", onClick: () => copyCoordsGmaps(ll) } : null
},
{ type: "sep" },
{ label: withIcon(ICONS.jump, "Jump / Search…"), sub: "coords • permalink • google maps • segment id", onClick: () => showJumpModal("") },
]);
}
function openPlaceMenu(x, y, ll, placeInfo) {
const has = !!ll;
try { menuState.rootLL = (ll || lastLonLat || null); menuState.rootType = "place"; } catch {}
const countTxt = placeInfo && Array.isArray(placeInfo.ids) && placeInfo.ids.length ? `${placeInfo.ids.length} selected` : "Selected";
// Keep the same actions as the Map menu; only change the header to "Place".
openRootMenu(x, y, "Place", countTxt, [
{ label: withIcon(ICONS.copy, "Copy coordinates"), sub: has ? `${fmt(ll.lat)}, ${fmt(ll.lon)}` : "Move mouse on map first", disabled: !has, onClick: () => copyCoordsLatLon(ll) },
{ label: withIcon(ICONS.mapPin, "Pin this Place"), sub: has ? "Save in pins panel" : "Move mouse on map first", disabled: !has, onClick: () => pinThisPlace("place", ll, null) },
{ label: withIcon(ICONS.refresh, "Refresh here"), sub: has ? "Reload editor at this spot" : "Move mouse on map first", disabled: !has, onClick: () => refreshHere(ll, null) },
{ label: withIcon(ICONS.chain, "Copy permalink"), sub: has ? "WME permalink (lat/lon/zoom)" : "Move mouse on map first", disabled: !has, onClick: () => copyPermalink(ll, null) },
{ label: withIcon(ICONS.ext, "Open in Google Maps"), sub: has ? "https://www.google.com/…" : "Move mouse on map first", disabled: !has,
onClick: () => { try { UW.open(gmapsUrlFromLonLat(ll), "_blank", "noopener,noreferrer"); } catch (e) { console.error(e); } closeAllMenus(); },
rightButton: has ? { label: "Copy", onClick: () => copyCoordsGmaps(ll) } : null
},
{ type: "sep" },
{ label: withIcon(ICONS.jump, "Jump / Search…"), sub: "coords • permalink • google maps • segment id", onClick: () => showJumpModal("") },
]);
}
function hitTestPinAtClientPoint(clientX, clientY, radiusPx = 18) {
try {
const t = document.elementFromPoint(clientX, clientY);
const el = t && t.closest ? t.closest(".wmeRcPinMarker, .wmeRcPinCluster") : null;
if (el && el.classList.contains("wmeRcPinMarker")) return { type: "pin", pinId: el.dataset.pinId };
if (el && el.classList.contains("wmeRcPinCluster")) return { type: "cluster", ids: (el.dataset.ids || "").split(",").filter(Boolean), lon: Number(el.dataset.lon), lat: Number(el.dataset.lat) };
} catch {}
try {
const mapEl = getMapContainerEl();
if (!mapEl) return null;
const r = mapEl.getBoundingClientRect();
const x = clientX - r.left;
const y = clientY - r.top;
if (x < 0 || y < 0 || x > r.width || y > r.height) return null;
const pins = loadPins().filter((p) => !(p.hideOnMap === true));
let best = null;
let bestD = Infinity;
for (const p of pins) {
const px = getMapPixelFromLonLat(p.lon, p.lat);
if (!px) continue;
const dx = px.x - x;
const dy = px.y - y;
const d = Math.hypot(dx, dy);
if (d < bestD) { bestD = d; best = p; }
}
if (best && bestD <= radiusPx) return { type: "pin", pinId: best.id };
} catch {}
return null;
}
function zoomToCluster(lat, lon) {
try {
const map = getOlMapBestEffort();
const ol = UW?.OpenLayers;
if (map && ol && typeof map.getZoom === "function" && typeof map.setCenter === "function") {
let ll = new ol.LonLat(lon, lat);
try {
const dst = map.getProjectionObject?.() || map.projection || null;
const dstCode = String(dst?.projCode || dst?.getCode?.() || dst || "");
const needsTransform = /900913|3857|102113|102100/i.test(dstCode) && !/4326/i.test(dstCode);
if (needsTransform && typeof ll.transform === "function") {
const src = new ol.Projection("EPSG:4326");
if (dst) ll.transform(src, dst);
}
} catch {}
const z0 = Number(map.getZoom());
const z = Number.isFinite(z0) ? z0 : 12;
map.setCenter(ll, Math.max(z + 3, 14));
setTimeout(() => { try { renderPinsMarkers(); } catch {} }, 260);
return;
}
} catch {}
try {
if (typeof jumpToCoords === "function") jumpToCoords(lat, lon, (getCurrentZoomBestEffort() || 12) + 2);
else zoomToLonLat(lon, lat);
} catch {}
}
function onContextMenuCapture(e) {
if (!enabled) return;
if (e.shiftKey) return;
if (!isMapClick(e.clientX, e.clientY)) return;
if (shouldAllowNativeContextMenu(e)) return;
e.preventDefault();
e.stopPropagation();
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
const segIds = selectedSegmentIds();
const placeInfo = (!segIds.length ? selectedPlaceInfo() : { has: false, ids: [] });
const ll = lonLatFromClick(e.clientX, e.clientY);
if (segIds.length) openSegmentMenu(e.clientX, e.clientY, segIds, ll);
else if (placeInfo && placeInfo.has) openPlaceMenu(e.clientX, e.clientY, ll, placeInfo);
else openMapMenu(e.clientX, e.clientY, ll);
}
function onMouseDownCapture(e) {
if (!enabled) return;
if (e.button !== 2) return;
if (e.shiftKey) return;
if (!isMapClick(e.clientX, e.clientY)) return;
if (shouldAllowNativeContextMenu(e)) return;
e.preventDefault();
e.stopPropagation();
if (e.stopImmediatePropagation) e.stopImmediatePropagation();
}
ensureCSS();
window.addEventListener("contextmenu", onContextMenuCapture, { capture: true });
window.addEventListener("mousedown", onMouseDownCapture, { capture: true });
let tinyFallbackToggleEl = null;
function setEnabled(v) {
enabled = !!v;
try {
if (tinyFallbackToggleEl) {
tinyFallbackToggleEl.querySelector(".wmeRcTinyDot")?.classList.toggle("off", !enabled);
tinyFallbackToggleEl.querySelector(".wmeRcTinyTxt").textContent = enabled ? "Right-click: ON" : "Right-click: OFF";
}
} catch {}
}
function createTinyFallbackToggle() {
if (tinyFallbackToggleEl && document.contains(tinyFallbackToggleEl)) return;
const el = document.createElement("div");
el.className = "wmeRcTinyToggle";
el.innerHTML = `<div class="wmeRcTinyDot"></div><div class="wmeRcTinyTxt">Right-click: ON</div>`;
el.addEventListener("click", () => {
setEnabled(!enabled);
toast(`${SCRIPT_NAME}: ${enabled ? "ON" : "OFF"}`);
closeAllMenus();
});
(document.body || document.documentElement).appendChild(el);
tinyFallbackToggleEl = el;
setEnabled(enabled);
}
function mountSidebarPanelBestEffort() {
try {
const ww = UW?.WazeWrap;
const addTab = ww?.Interface?.AddScriptTab;
if (typeof addTab === "function") {
const tab = addTab(SCRIPT_NAME);
if (tab && tab.appendChild) {
const wrap = document.createElement("div");
wrap.className = "wmeRcSideWrap";
wrap.innerHTML = `
<div class="wmeRcSideCard">
<div class="wmeRcSideRow">
<div>
<div class="wmeRcSideTitle">Right-click menu</div>
<div class="wmeRcSideSub">Shift + right click = default menu</div>
</div>
<div class="wmeRcSwitch ${enabled ? "on" : ""}" id="wmeRcSideSwitch">
<div class="wmeRcKnob"></div>
</div>
</div>
</div>
<div class="wmeRcSideCard">
<div class="wmeRcSideRow">
<div>
<div class="wmeRcSideTitle">Pins on map</div>
<div class="wmeRcSideSub">Show / hide all pinned markers</div>
</div>
<div class="wmeRcSwitch ${getPinsLayerVisible() ? "on" : ""}" id="wmeRcPinsLayerSwitch">
<div class="wmeRcKnob"></div>
</div>
</div>
</div>
`;
tab.appendChild(wrap);
const sw = wrap.querySelector("#wmeRcSideSwitch");
sw.addEventListener("click", () => {
setEnabled(!enabled);
sw.classList.toggle("on", enabled);
toast(`${SCRIPT_NAME}: ${enabled ? "ON" : "OFF"}`);
closeAllMenus();
});
const ps = wrap.querySelector("#wmeRcPinsLayerSwitch");
if (ps) {
const applyPinsSwitch = () => {
const v = getPinsLayerVisible();
ps.classList.toggle("on", v);
};
applyPinsSwitch();
ps.addEventListener("click", () => {
const next = !getPinsLayerVisible();
setPinsLayerVisible(next);
ps.classList.toggle("on", next);
toast(`Pins on map: ${next ? "ON" : "OFF"}`);
});
}
if (tinyFallbackToggleEl) tinyFallbackToggleEl.remove();
tinyFallbackToggleEl = null;
return true;
}
}
} catch (e) {
console.warn("Sidebar mount failed:", e);
}
createTinyFallbackToggle();
return false;
}
async function initSdk() {
try {
sdk = UW.getWmeSdk({ scriptId: SCRIPT_ID, scriptName: SCRIPT_NAME });
} catch (err) {
console.warn(`[${SCRIPT_NAME}] getWmeSdk failed`, err);
mountSidebarPanelBestEffort();
return;
}
try { maybeShowChangelogOnce(); } catch {}
try {
const lvl = getUserLevel();
if (Number.isFinite(lvl)) cachedUserLevel = lvl;
} catch {}
try {
sdk.Events?.on?.({
eventName: "wme-map-mouse-move",
eventHandler: (ev) => {
if (ev && isFinite(ev.lon) && isFinite(ev.lat)) lastLonLat = { lon: ev.lon, lat: ev.lat };
},
});
} catch {}
try { ensurePinsPanel(); } catch {}
try { renderPinsMarkers(); } catch {}
try {
let tries = 0;
const iv = setInterval(() => {
tries++;
try { ensurePinsOlLayer(); } catch {}
try { startPinsMapSync(); } catch {}
if (pinsOlLayer || tries > 20) { try { clearInterval(iv); } catch {} }
}, 750);
} catch {}
try { startReminderLoop(); } catch {}
const tryMount = () => mountSidebarPanelBestEffort();
const t = setInterval(() => {
if (document.body) {
tryMount();
clearInterval(t);
}
}, 450);
setTimeout(() => clearInterval(t), 8000);
}
if (UW.SDK_INITIALIZED?.then) UW.SDK_INITIALIZED.then(initSdk);
else {
const t = setInterval(() => {
if (UW.SDK_INITIALIZED?.then) { clearInterval(t); UW.SDK_INITIALIZED.then(initSdk); }
}, 250);
setTimeout(() => clearInterval(t), 12000);
}
})();