// Drawing Manager — integrates lightweight-charts-drawing library
// Features: click-to-create, drag, settings popup, price alerts, fib levels/colors
const DM = (() => {
const LCD = window.LightweightChartsDrawing;
if (!LCD) {
console.warn('[DM] LightweightChartsDrawing library not loaded');
return null;
}
const registry = LCD.getToolRegistry();
if (!registry) {
console.warn('[DM] ToolRegistry not available');
return null;
}
const STORE_KEY = 'fs_dm_drawings';
const ALERT_KEY = 'fs_dm_alerts'; // {drawingId: {sym, price, color}}
const FIB_CFG_KEY = 'fs_fib_config'; // {levels: [...], colors: {...}}
let manager = null;
let currentSym = null;
let currentChart = null;
let currentSeries = null;
let currentContainer = null;
// Tool state
let activeTool = null;
let pendingAnchors = [];
let requiredAnchors = 0;
let _onToolDone = null;
// Our toolbar IDs → library tool types
const TOOL_MAP = {
'hline': 'horizontal-line',
'ray': 'ray', // 2-anchor ray (extend right), was horizontal-ray
'hray': 'horizontal-ray', // 1-anchor horizontal ray (kept for backwards compat)
'trendline': 'trend-line',
'extline': 'extended-line', // infinite both directions
'fib': 'fib-retracement',
'rect': 'rectangle',
'cursor': null,
};
// Default styles
const DEFAULT_STYLES = {
'horizontal-line': { lineColor: '#2962FF', lineWidth: 2 },
'horizontal-ray': { lineColor: '#FF6D00', lineWidth: 2 },
'ray': { lineColor: '#FF6D00', lineWidth: 2 },
'trend-line': { lineColor: '#2962FF', lineWidth: 2 },
'extended-line': { lineColor: '#2962FF', lineWidth: 1, lineDash: [6, 3] },
'fib-retracement': { lineColor: '#9C27B0', lineWidth: 1 },
'rectangle': { lineColor: '#00BCD4', lineWidth: 1, fillColor: 'rgba(0,188,212,0.15)' },
};
const COLOR_PRESETS = [
'#2962FF', '#FF6D00', '#E91E63', '#00BCD4',
'#4CAF50', '#FF5252', '#9C27B0', '#FFD600',
'#FFFFFF', '#B0BEC5',
];
const WIDTH_OPTIONS = [1, 2, 3, 4];
const DEFAULT_FIB_LEVELS = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1, 1.618, 2.618];
const DEFAULT_FIB_COLORS = {
'0': '#787B86', '0.236': '#F44336', '0.382': '#4CAF50', '0.5': '#FF9800',
'0.618': '#2962FF', '0.786': '#9C27B0', '1': '#787B86',
'1.618': '#00BCD4', '2.618': '#E91E63',
};
let _clickUnsub = null;
let _idCounter = 0;
let _settingsEl = null;
let _hoveredDrawing = null;
let _hoverHandler = null;
let _ctxMenuHandler = null;
let _keyHandler = null;
// Undo/redo snapshot stack (stores state BEFORE each change)
const _undoStack = [];
const _redoStack = [];
const MAX_UNDO = 30;
let _lastState = null; // JSON string of drawings before latest change
function _genId() { return `dm_${Date.now()}_${++_idCounter}`; }
// ── Fib Config Persistence ──────────────────────────
function _loadFibConfig() {
try {
const raw = localStorage.getItem(FIB_CFG_KEY);
if (raw) {
const c = JSON.parse(raw);
return {
levels: c.levels || DEFAULT_FIB_LEVELS,
colors: c.colors || DEFAULT_FIB_COLORS,
};
}
} catch (e) {}
return { levels: [...DEFAULT_FIB_LEVELS], colors: { ...DEFAULT_FIB_COLORS } };
}
function _saveFibConfig(cfg) {
try { localStorage.setItem(FIB_CFG_KEY, JSON.stringify(cfg)); } catch (e) {}
}
// ── Alert Persistence ───────────────────────────────
function _loadAlerts() {
try { return JSON.parse(localStorage.getItem(ALERT_KEY) || '{}'); } catch { return {}; }
}
function _saveAlerts(alerts) {
try { localStorage.setItem(ALERT_KEY, JSON.stringify(alerts)); } catch (e) {}
}
// ── Attach / Detach ─────────────────────────────────
function attach(chart, series, container, sym, onToolDone) {
detach();
currentChart = chart;
currentSeries = series;
currentContainer = container;
currentSym = sym;
_onToolDone = onToolDone || null;
try {
manager = new LCD.DrawingManager();
manager.attach(chart, series, container);
_clickUnsub = (param) => _handleChartClick(param);
chart.subscribeClick(_clickUnsub);
manager.on('drawing:added', () => { _snapshot(); _save(sym); });
manager.on('drawing:updated', () => { _snapshot(); _save(sym); });
manager.on('drawing:removed', () => { _snapshot(); _save(sym); });
// Hover highlight — library renders control points on 'hovered' state
_hoverHandler = (e) => _handleMouseMove(e);
container.addEventListener('mousemove', _hoverHandler);
// Context menu — right-click on drawing → settings popup
_ctxMenuHandler = (e) => _handleContextMenu(e);
container.addEventListener('contextmenu', _ctxMenuHandler);
// Undo/redo keyboard shortcuts (Ctrl+Z / Ctrl+Shift+Z)
_keyHandler = (e) => _handleKeyDown(e);
document.addEventListener('keydown', _keyHandler);
_load(sym);
// Capture initial state as baseline for undo
try { _lastState = JSON.stringify(manager.exportDrawings()); } catch (e) {}
console.log(`[DM] Attached to ${sym}`);
} catch (e) {
console.error('[DM] Attach failed:', e);
manager = null;
}
}
function detach() {
_cancelPending();
_hideSettings();
if (currentContainer && _hoverHandler) {
currentContainer.removeEventListener('mousemove', _hoverHandler);
}
if (currentContainer && _ctxMenuHandler) {
currentContainer.removeEventListener('contextmenu', _ctxMenuHandler);
}
if (_keyHandler) {
document.removeEventListener('keydown', _keyHandler);
}
_hoverHandler = null;
_ctxMenuHandler = null;
_keyHandler = null;
_hoveredDrawing = null;
_undoStack.length = 0;
_redoStack.length = 0;
_lastState = null;
if (manager && currentChart && _clickUnsub) {
try { currentChart.unsubscribeClick(_clickUnsub); } catch (e) {}
}
if (manager) {
if (currentSym) _save(currentSym);
try { manager.detach(); } catch (e) {}
manager = null;
}
_clickUnsub = null;
currentChart = null;
currentSeries = null;
currentContainer = null;
activeTool = null;
_onToolDone = null;
}
// ── Tool Selection ──────────────────────────────────
function setTool(toolId) {
if (!manager) return false;
_cancelPending();
_hideSettings();
const libType = TOOL_MAP[toolId];
if (toolId === 'cursor' || libType === undefined) {
activeTool = null;
manager.setActiveTool(null);
return true;
}
const def = registry.get(libType);
if (!def) return false;
activeTool = libType;
requiredAnchors = def.requiredAnchors;
pendingAnchors = [];
manager.setActiveTool(libType);
return true;
}
// ── Time Extrapolation (future area where coordinateToTime returns null) ──
function _getTimeFromX(x) {
if (!currentChart) return null;
const ts = currentChart.timeScale();
const t = ts.coordinateToTime(x);
if (t !== null) return t;
// Extrapolate: find two known bars and project forward
let ref1 = null, ref2 = null;
for (let testX = Math.floor(x) - 1; testX >= 0; testX -= 5) {
const tt = ts.coordinateToTime(testX);
if (tt !== null) {
if (!ref1) { ref1 = { x: testX, t: tt }; }
else if (tt !== ref1.t) { ref2 = { x: testX, t: tt }; break; }
}
}
if (!ref1 || !ref2) return null;
const pxPerSec = (ref1.x - ref2.x) / (ref1.t - ref2.t);
if (pxPerSec <= 0) return null;
return Math.round(ref1.t + (x - ref1.x) / pxPerSec);
}
// ── Hover Highlight ───────────────────────────────────
function _handleMouseMove(e) {
if (!manager || !currentContainer || activeTool) return;
const rect = currentContainer.getBoundingClientRect();
const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
const hit = manager.hitTest(point);
if (hit !== _hoveredDrawing) {
// Reset previous
if (_hoveredDrawing) {
try {
const sel = manager.getSelectedDrawing();
_hoveredDrawing.setState(sel && sel.id === _hoveredDrawing.id ? 'selected' : 'normal');
} catch (err) {}
}
// Set new
if (hit) {
try {
const sel = manager.getSelectedDrawing();
if (!sel || sel.id !== hit.id) hit.setState('hovered');
} catch (err) {}
currentContainer.style.cursor = 'pointer';
} else {
currentContainer.style.cursor = '';
}
_hoveredDrawing = hit;
}
}
// ── Context Menu (right-click on drawing) ────────────
function _handleContextMenu(e) {
if (!manager || !currentContainer) return;
const rect = currentContainer.getBoundingClientRect();
const point = { x: e.clientX - rect.left, y: e.clientY - rect.top };
const hit = manager.hitTest(point);
if (!hit) return;
e.preventDefault();
// Select the drawing and show settings popup
try { manager.selectDrawing(hit.id); } catch (err) {}
_showSettings(point.x, point.y, hit);
}
// ── Click Handler ───────────────────────────────────
function _handleChartClick(param) {
if (!activeTool) {
_handleSelectionClick(param);
return;
}
if (!manager || !currentSeries) return;
if (!param.time && !param.point) return;
let time = param.time;
// Extrapolate time when clicking in the future area (beyond last candle)
if (time == null && param.point) {
time = _getTimeFromX(param.point.x);
}
let price = param.point ? currentSeries.coordinateToPrice(param.point.y) : null;
if (time == null || price == null) return;
pendingAnchors.push({ time, price });
if (pendingAnchors.length >= requiredAnchors) {
_createDrawing(activeTool, [...pendingAnchors]);
pendingAnchors = [];
_resetToCursor();
}
}
function _handleSelectionClick(param) {
if (!manager || !param.point) return;
setTimeout(() => {
const sel = manager.getSelectedDrawing ? manager.getSelectedDrawing() : null;
if (sel) {
_showSettings(param.point.x, param.point.y, sel);
} else {
_hideSettings();
}
}, 50);
}
function _createDrawing(type, anchors) {
const id = _genId();
const style = { ...(DEFAULT_STYLES[type] || {}) };
const opts = {};
// Apply custom fib config
if (type === 'fib-retracement') {
const cfg = _loadFibConfig();
opts.levels = cfg.levels;
}
try {
const drawing = registry.createDrawing(type, id, anchors, style, opts);
if (drawing) {
// Apply per-level fib colors
if (type === 'fib-retracement' && drawing.setFibOptions) {
const cfg = _loadFibConfig();
// Library uses levels array, colors applied via line style per level
drawing.setFibOptions({ levels: cfg.levels });
}
manager.addDrawing(drawing);
}
} catch (e) {
console.error(`[DM] Failed to create ${type}:`, e);
}
}
function _resetToCursor() {
activeTool = null;
pendingAnchors = [];
if (manager) manager.setActiveTool(null);
if (_onToolDone) _onToolDone();
}
function _cancelPending() { pendingAnchors = []; }
// ── Settings Popup ──────────────────────────────────
function _showSettings(x, y, drawing) {
_hideSettings();
if (!currentContainer) return;
const popup = document.createElement('div');
popup.className = 'dm-settings';
const cRect = currentContainer.getBoundingClientRect();
let left = Math.min(x + 10, cRect.width - 200);
let top = Math.max(y - 100, 10);
popup.style.cssText = `position:absolute;left:${left}px;top:${top}px;z-index:1000;
background:#1e222d;border:1px solid #363a45;border-radius:8px;padding:10px;
display:flex;flex-direction:column;gap:6px;box-shadow:0 4px 16px rgba(0,0,0,0.5);
min-width:170px;font-size:12px;`;
// ─ Color row
const colorRow = document.createElement('div');
colorRow.style.cssText = 'display:flex;gap:4px;flex-wrap:wrap;';
const currentColor = drawing._style?.lineColor || '#2962FF';
COLOR_PRESETS.forEach(c => {
const sw = document.createElement('div');
sw.style.cssText = `width:20px;height:20px;border-radius:4px;cursor:pointer;
background:${c};border:2px solid ${c === currentColor ? '#fff' : 'transparent'};`;
sw.onclick = (e) => {
e.stopPropagation();
try { drawing.applyStyle({ lineColor: c }); } catch (err) {}
_save(currentSym);
_showSettings(x, y, drawing);
};
colorRow.appendChild(sw);
});
popup.appendChild(colorRow);
// ─ Width row
const widthRow = document.createElement('div');
widthRow.style.cssText = 'display:flex;gap:4px;align-items:center;';
const wl = document.createElement('span');
wl.textContent = 'Width';
wl.style.cssText = 'color:#787b86;font-size:11px;margin-right:4px;';
widthRow.appendChild(wl);
const currentWidth = drawing._style?.lineWidth || 2;
WIDTH_OPTIONS.forEach(w => {
const btn = document.createElement('div');
btn.style.cssText = `width:24px;height:20px;display:flex;align-items:center;justify-content:center;
cursor:pointer;border-radius:4px;font-size:11px;color:#d1d4dc;
background:${w === currentWidth ? '#363a45' : 'transparent'};`;
btn.textContent = w;
btn.onclick = (e) => {
e.stopPropagation();
try { drawing.applyStyle({ lineWidth: w }); } catch (err) {}
_save(currentSym);
_showSettings(x, y, drawing);
};
widthRow.appendChild(btn);
});
popup.appendChild(widthRow);
// ─ Alert toggle (for line-type drawings)
const isLine = ['horizontal-line', 'horizontal-ray', 'trend-line'].includes(drawing.type);
if (isLine) {
const alerts = _loadAlerts();
const hasAlert = !!alerts[drawing.id];
const alertRow = document.createElement('div');
alertRow.style.cssText = 'display:flex;align-items:center;gap:6px;cursor:pointer;padding:2px 0;';
alertRow.innerHTML = `<span style="font-size:14px">${hasAlert ? '🔔' : '🔕'}</span>
<span style="color:${hasAlert ? '#FFD600' : '#787b86'}">Price Alert ${hasAlert ? 'ON' : 'OFF'}</span>`;
alertRow.onclick = (e) => {
e.stopPropagation();
const al = _loadAlerts();
if (al[drawing.id]) {
delete al[drawing.id];
} else {
// Get price from first anchor
const price = drawing._anchors?.[0]?.price;
const color = drawing._style?.lineColor || '#2962FF';
if (price != null) {
al[drawing.id] = { sym: currentSym, price, color };
}
}
_saveAlerts(al);
_showSettings(x, y, drawing);
};
popup.appendChild(alertRow);
}
// ─ Fib settings (for fib-retracement)
if (drawing.type === 'fib-retracement') {
_appendFibSettings(popup, drawing);
}
// ─ Delete button
const delRow = document.createElement('div');
delRow.style.cssText = `display:flex;align-items:center;justify-content:center;gap:4px;
padding:4px 0;cursor:pointer;color:#FF5252;border-top:1px solid #363a45;
margin-top:2px;padding-top:6px;`;
delRow.innerHTML = '🗑 Delete';
delRow.onclick = (e) => {
e.stopPropagation();
// Clean up alert if exists
const al = _loadAlerts();
if (al[drawing.id]) { delete al[drawing.id]; _saveAlerts(al); }
manager.removeDrawing(drawing.id);
_save(currentSym);
_hideSettings();
};
popup.appendChild(delRow);
// Close on outside click (persistent until popup is closed)
setTimeout(() => {
popup._closeHandler = (e) => {
if (!popup.contains(e.target)) {
_hideSettings();
document.removeEventListener('click', popup._closeHandler, { capture: true });
}
};
document.addEventListener('click', popup._closeHandler, { capture: true });
}, 100);
currentContainer.appendChild(popup);
_settingsEl = popup;
}
// ── Fib Level Settings ──────────────────────────────
function _appendFibSettings(popup, drawing) {
const cfg = _loadFibConfig();
const header = document.createElement('div');
header.style.cssText = 'color:#787b86;font-size:11px;border-top:1px solid #363a45;padding-top:6px;margin-top:2px;';
header.textContent = 'Fibonacci Levels';
popup.appendChild(header);
const levelsWrap = document.createElement('div');
levelsWrap.style.cssText = 'display:flex;flex-direction:column;gap:3px;max-height:200px;overflow-y:auto;';
cfg.levels.forEach((lvl, i) => {
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:4px;';
// Color swatch for this level
const key = String(lvl);
const lvlColor = cfg.colors[key] || '#787B86';
const colorBtn = document.createElement('div');
colorBtn.style.cssText = `width:16px;height:16px;border-radius:3px;cursor:pointer;
background:${lvlColor};border:1px solid #555;flex-shrink:0;`;
colorBtn.title = `Color for ${lvl}`;
colorBtn.onclick = (e) => {
e.stopPropagation();
_showColorPicker(colorBtn, lvlColor, (newColor) => {
cfg.colors[key] = newColor;
_saveFibConfig(cfg);
// Update drawing if possible
_applyFibColors(drawing, cfg);
colorBtn.style.background = newColor;
});
};
row.appendChild(colorBtn);
// Level value
const valSpan = document.createElement('span');
valSpan.style.cssText = 'color:#d1d4dc;font-size:11px;min-width:40px;';
valSpan.textContent = lvl === 0 ? '0' : lvl;
row.appendChild(valSpan);
// Remove button
if (lvl !== 0 && lvl !== 1) {
const rmBtn = document.createElement('span');
rmBtn.style.cssText = 'color:#FF5252;cursor:pointer;font-size:13px;margin-left:auto;';
rmBtn.textContent = '✕';
rmBtn.onclick = (e) => {
e.stopPropagation();
cfg.levels.splice(i, 1);
delete cfg.colors[key];
_saveFibConfig(cfg);
if (drawing.setFibOptions) drawing.setFibOptions({ levels: cfg.levels });
_save(currentSym);
// Refresh popup
_hideSettings();
};
row.appendChild(rmBtn);
}
levelsWrap.appendChild(row);
});
popup.appendChild(levelsWrap);
// Add level button
const addRow = document.createElement('div');
addRow.style.cssText = 'display:flex;gap:4px;align-items:center;';
const inp = document.createElement('input');
inp.type = 'number';
inp.step = '0.01';
inp.placeholder = 'e.g. 1.272';
inp.style.cssText = 'width:70px;background:#131722;border:1px solid #363a45;border-radius:4px;color:#d1d4dc;padding:2px 4px;font-size:11px;';
const addBtn = document.createElement('span');
addBtn.textContent = '+ Add';
addBtn.style.cssText = 'color:#4CAF50;cursor:pointer;font-size:11px;';
addBtn.onclick = (e) => {
e.stopPropagation();
const v = parseFloat(inp.value);
if (isNaN(v) || cfg.levels.includes(v)) return;
cfg.levels.push(v);
cfg.levels.sort((a, b) => a - b);
cfg.colors[String(v)] = '#2962FF';
_saveFibConfig(cfg);
if (drawing.setFibOptions) drawing.setFibOptions({ levels: cfg.levels });
_save(currentSym);
_hideSettings();
};
addRow.appendChild(inp);
addRow.appendChild(addBtn);
// Reset button
const resetBtn = document.createElement('span');
resetBtn.textContent = 'Reset';
resetBtn.style.cssText = 'color:#787b86;cursor:pointer;font-size:11px;margin-left:auto;';
resetBtn.onclick = (e) => {
e.stopPropagation();
_saveFibConfig({ levels: [...DEFAULT_FIB_LEVELS], colors: { ...DEFAULT_FIB_COLORS } });
if (drawing.setFibOptions) drawing.setFibOptions({ levels: DEFAULT_FIB_LEVELS });
_save(currentSym);
_hideSettings();
};
addRow.appendChild(resetBtn);
popup.appendChild(addRow);
}
function _applyFibColors(drawing, cfg) {
// Library uses a single lineColor style; per-level colors need to be set via levels array
// The library doesn't natively support per-level colors, but we store them for reference
// Apply the primary color from the 0.618 level as main lineColor
const primary = cfg.colors['0.618'] || cfg.colors['0.5'] || '#2962FF';
try { drawing.applyStyle({ lineColor: primary }); } catch (e) {}
_save(currentSym);
}
// Mini color picker dropdown
function _showColorPicker(anchor, currentColor, onPick) {
// Remove existing picker
document.querySelectorAll('.dm-color-picker').forEach(el => el.remove());
const picker = document.createElement('div');
picker.className = 'dm-color-picker';
const rect = anchor.getBoundingClientRect();
const cRect = currentContainer.getBoundingClientRect();
picker.style.cssText = `position:absolute;left:${rect.left - cRect.left}px;top:${rect.bottom - cRect.top + 4}px;
z-index:1001;background:#1e222d;border:1px solid #363a45;border-radius:6px;padding:6px;
display:flex;gap:3px;flex-wrap:wrap;width:130px;box-shadow:0 4px 12px rgba(0,0,0,0.5);`;
COLOR_PRESETS.forEach(c => {
const sw = document.createElement('div');
sw.style.cssText = `width:18px;height:18px;border-radius:3px;cursor:pointer;
background:${c};border:2px solid ${c === currentColor ? '#fff' : 'transparent'};`;
sw.onclick = (e) => {
e.stopPropagation();
onPick(c);
picker.remove();
};
picker.appendChild(sw);
});
setTimeout(() => {
const handler = (e) => {
if (!picker.contains(e.target)) picker.remove();
document.removeEventListener('click', handler, true);
};
document.addEventListener('click', handler, { capture: true });
}, 50);
currentContainer.appendChild(picker);
}
function _hideSettings() {
if (_settingsEl) {
if (_settingsEl._closeHandler) {
document.removeEventListener('click', _settingsEl._closeHandler, { capture: true });
}
_settingsEl.remove();
_settingsEl = null;
}
document.querySelectorAll('.dm-color-picker').forEach(el => el.remove());
}
// ── Undo / Redo ─────────────────────────────────────
function _snapshot() {
if (!manager) return;
try {
// Push previous state (before this change) to undo stack
if (_lastState !== null) {
_undoStack.push(_lastState);
if (_undoStack.length > MAX_UNDO) _undoStack.shift();
_redoStack.length = 0;
}
// Update last known state to current (after change)
_lastState = JSON.stringify(manager.exportDrawings());
} catch (e) {}
}
function undo() {
if (!manager || !_undoStack.length) return;
try {
// Save current state to redo
_redoStack.push(JSON.stringify(manager.exportDrawings()));
const prev = JSON.parse(_undoStack.pop());
manager.clearAll();
manager.importDrawings(prev);
if (currentSym) _save(currentSym);
} catch (e) { console.warn('[DM] Undo failed:', e); }
}
function redo() {
if (!manager || !_redoStack.length) return;
try {
_undoStack.push(JSON.stringify(manager.exportDrawings()));
const next = JSON.parse(_redoStack.pop());
manager.clearAll();
manager.importDrawings(next);
if (currentSym) _save(currentSym);
} catch (e) { console.warn('[DM] Redo failed:', e); }
}
function _handleKeyDown(e) {
if (!manager) return;
// Ctrl+Z / Cmd+Z = undo, Ctrl+Shift+Z / Cmd+Shift+Z = redo
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
e.preventDefault();
if (e.shiftKey) redo(); else undo();
}
}
// ── Actions ─────────────────────────────────────────
function clearAll() {
if (!manager) return;
_cancelPending();
_hideSettings();
// Clean up all alerts for this symbol
const al = _loadAlerts();
let changed = false;
for (const [id, a] of Object.entries(al)) {
if (a.sym === currentSym) { delete al[id]; changed = true; }
}
if (changed) _saveAlerts(al);
manager.clearAll();
if (currentSym) _save(currentSym);
}
function deleteSelected() {
if (!manager) return;
_hideSettings();
const sel = manager.getSelectedDrawing();
if (sel) {
const al = _loadAlerts();
if (al[sel.id]) { delete al[sel.id]; _saveAlerts(al); }
manager.removeDrawing(sel.id);
if (currentSym) _save(currentSym);
}
}
// ── Price Alert Checking (called from WS tick) ──────
const alertCooldowns = {};
const COOLDOWN_MS = 60000;
const lastAlertPrices = {};
function checkAlerts(sym, currentPrice) {
const alerts = _loadAlerts();
const prevPrice = lastAlertPrices[sym];
lastAlertPrices[sym] = currentPrice;
if (prevPrice === undefined) return;
for (const [drawingId, al] of Object.entries(alerts)) {
if (al.sym !== sym) continue;
const alertPrice = al.price;
const crossedUp = prevPrice < alertPrice && currentPrice >= alertPrice;
const crossedDown = prevPrice > alertPrice && currentPrice <= alertPrice;
if (!crossedUp && !crossedDown) continue;
const coolKey = `${drawingId}`;
const now = Date.now();
if (alertCooldowns[coolKey] && now - alertCooldowns[coolKey] < COOLDOWN_MS) continue;
alertCooldowns[coolKey] = now;
const dir = crossedUp ? '▲ Above' : '▼ Below';
const ticker = sym.replace('USDT', '');
_fireAlert(sym, ticker, currentPrice, alertPrice, dir, al.color);
}
}
function _fireAlert(sym, ticker, price, level, dir, color) {
// Reuse existing showAlertToast if available (from mini-charts.js)
if (typeof showAlertToast === 'function') {
showAlertToast(sym, ticker, price, level, dir, color);
return;
}
// Fallback: browser notification
if (Notification.permission === 'granted') {
new Notification(`🔔 ${ticker} ${dir} $${level.toFixed(2)}`, {
body: `Price: $${price.toFixed(2)}`,
icon: '/icon-192.png',
});
}
}
// ── Persistence ─────────────────────────────────────
function _loadStore() {
try { return JSON.parse(localStorage.getItem(STORE_KEY) || '{}'); } catch { return {}; }
}
function _save(sym) {
if (!manager || !sym) return;
try {
const store = _loadStore();
store[sym] = manager.exportDrawings();
localStorage.setItem(STORE_KEY, JSON.stringify(store));
} catch (e) { console.warn('[DM] Save failed:', e); }
}
function _load(sym) {
if (!manager || !sym) return;
try {
const store = _loadStore();
const data = store[sym];
if (data) manager.importDrawings(data);
} catch (e) { console.warn('[DM] Load failed:', e); }
}
// ── Public API ──────────────────────────────────────
return {
attach, detach, setTool, clearAll, deleteSelected, checkAlerts, undo, redo,
isActive: () => !!manager,
getActiveTool: () => activeTool,
getManager: () => manager,
isDragging: () => manager ? manager._isDragging : false,
TOOL_MAP,
};
})();
📜 Git History
7a5cb1ffix: drawing tools — touch support, future area, magnet snap, SW cache7 weeks ago
64a76d9fix: drawing settings popup close handler — remove once:true2 months ago
74bc0b7feat: library drawing tools — lightweight-charts-drawing integration2 months ago
b69752dwip: LWC v4→v5 migration + drawing library integration (modal chart bug unresolved)2 months ago
Show last diff
Loading...