Multi-checklist action panel for Torn.com
// ==UserScript==
// @name OpsCheck
// @version 2.5.2
// @description Multi-checklist action panel for Torn.com
// @author BBSmalls
// @match https://www.torn.com/*
// @grant none
// @namespace https://www.torn.com/
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'opsCheck';
const MIN_WIDTH = 180;
const DEFAULT_LIST = {
id: 'war-energy',
name: 'War Energy',
buttons: [
{ name: 'Refill', url: 'https://www.torn.com/page.php?sid=points', done: false, enabled: true },
{ name: 'Stocks', url: 'https://www.torn.com/page.php?sid=stocks', done: false, enabled: true },
{ name: 'Xanax', url: 'https://www.torn.com/factions.php?step=your&type=1#/tab=armoury&start=0&sub=drugs', done: false, enabled: true },
{ name: 'Energy Cans', url: 'https://www.torn.com/item.php', done: false, enabled: true },
{ name: 'Job Points', url: 'https://www.torn.com/companies.php?step=your&type=1', done: false, enabled: true },
]
};
// ─── Load State ───────────────────────────────────────────────────────────
function loadState() {
let saved = JSON.parse(localStorage.getItem(STORAGE_KEY));
// ── Backward compatibility: old warEnergy format ──
if (!saved) {
const old = JSON.parse(localStorage.getItem('warEnergy'));
if (old && old.buttons && old.buttons.length > 0) {
saved = {
lists: [{ id: 'war-energy', name: 'War Energy', buttons: old.buttons }],
activeListId: 'war-energy',
pos: old.pos || { x: 20, y: 20 },
size: old.size || { w: 280, h: 300 },
locked: old.locked || false,
minimized: old.minimized || false,
fabPos: { x: 16, y: window.innerHeight - 60 },
panelVisible: true
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(saved));
return saved;
}
}
// ── Fresh install ──
if (!saved) {
const fresh = {
lists: [{ id: 'war-energy', name: 'War Energy', buttons: DEFAULT_LIST.buttons.map(b => ({ ...b })) }],
activeListId: 'war-energy',
pos: { x: 20, y: 20 },
size: { w: 280, h: 300 },
locked: false,
minimized: false,
fabPos: { x: 16, y: window.innerHeight - 60 },
panelVisible: true
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(fresh));
return fresh;
}
// ── Existing opsCheck save ──
if (!saved.lists) saved.lists = [{ id: 'war-energy', name: 'War Energy', buttons: [] }];
if (!saved.activeListId) saved.activeListId = saved.lists[0].id;
return saved;
}
let state = loadState();
if (!state.fabPos) state.fabPos = { x: 16, y: window.innerHeight - 60 };
if (state.panelVisible === undefined) state.panelVisible = true;
if (state.hideOnAttack === undefined) state.hideOnAttack = true;
state.lists.forEach(l => { if (l.enabled === undefined) l.enabled = true; });
function activeList() {
const found = state.lists.find(l => l.id === state.activeListId);
if (found && found.enabled) return found;
const firstEnabled = state.lists.find(l => l.enabled);
if (firstEnabled) { state.activeListId = firstEnabled.id; return firstEnabled; }
return null;
}
function saveState() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
// ─── Styles ───────────────────────────────────────────────────────────────
const style = document.createElement('style');
style.textContent = `
#we-panel {
position: fixed;
z-index: 99999;
background: #111416;
border: 1px solid #2a2f35;
border-radius: 6px;
box-shadow: 0 4px 24px rgba(0,0,0,0.7), inset 0 1px 0 rgba(255,255,255,0.04);
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-size: 12px;
color: #c8cdd4;
display: flex;
flex-direction: column;
min-width: ${MIN_WIDTH}px;
overflow: hidden;
user-select: none;
}
#we-titlebar {
background: #1a1e23;
border-bottom: 1px solid #2a2f35;
padding: 5px 8px;
cursor: grab;
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
#we-titlebar:active { cursor: grabbing; }
#we-title-icon {
width: 12px; height: 12px;
background: linear-gradient(135deg, #e8a020, #c06000);
border-radius: 2px;
flex-shrink: 0;
}
#we-title-text {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #9da6b0;
flex: 1;
}
/* Brighter Minimize Button */
#we-btn-minimize {
background: none;
border: none;
cursor: pointer;
font-size: 15px;
padding: 0 2px 2px;
line-height: 1;
color: #b8c8ff;
font-weight: bold;
opacity: 0.9;
transition: all 0.15s;
}
#we-btn-minimize:hover {
color: #ffffff;
opacity: 1;
transform: scale(1.1);
}
#we-upper {
flex: 1;
overflow-y: auto;
padding: 6px 6px 2px;
display: flex;
flex-direction: column;
gap: 4px;
min-height: 40px;
}
#we-selector {
display: flex;
align-items: center;
gap: 4px;
padding: 5px 6px;
border-bottom: 1px solid #1e2328;
background: #0e1114;
flex-shrink: 0;
}
#we-selector select {
flex: 1;
min-width: 0;
width: 0;
background: #1a1e23;
border: 1px solid #2a2f35;
border-radius: 4px;
color: #c8cdd4;
font-size: 11px;
padding: 4px 6px;
cursor: pointer;
outline: none;
}
#we-selector select:focus { border-color: #5080b0; }
#we-selector option { background: #1a1e23; color: #c8cdd4; }
.we-sel-btn {
width: 24px; height: 24px;
border: 1px solid #2a2f35;
border-radius: 4px;
background: #1a1e23;
color: #7a8490;
font-size: 12px;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background 0.1s, color 0.1s;
padding: 0;
flex-shrink: 0;
}
.we-sel-btn:hover { background: #252d38; color: #c8cdd4; }
.we-sel-btn.danger:hover { background: #2a1a1a; color: #c96b6b; border-color: #5a3a3a; }
.we-row.drag-over-top { border-top: 2px solid #5080b0; }
.we-row.drag-over-bot { border-bottom: 2px solid #5080b0; }
.we-edit-controls {
display: flex;
flex-direction: row;
gap: 3px;
flex-shrink: 0;
}
#we-panel.minimized {
min-height: 0;
}
.we-row {
display: flex;
gap: 6px;
align-items: center;
}
/* Main Panel Buttons - Dark Green */
.we-action-btn {
flex: 1;
padding: 7px 10px;
border: 2px solid #006633;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
min-width: 0;
border-radius: 6px;
background: #0f2a1f;
color: #00ff99;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
box-shadow: 0 0 8px rgba(0, 170, 68, 0.35);
}
.we-action-btn:hover {
background: #163829;
border-color: #00aa66;
color: #ffffff;
transform: translateY(-1px);
box-shadow: 0 0 12px rgba(0, 204, 102, 0.5);
}
.we-action-btn:disabled {
border-color: #666677;
background: #1f2228;
color: #778899;
text-decoration: line-through;
opacity: 0.75;
}
/* Edit Mode */
.we-action-btn-edit {
flex: 1;
padding: 7px 10px;
border: 2px solid #557799;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
border-radius: 6px;
background: #1c242f;
color: #bbddff;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
}
.we-action-btn-edit.enabled {
border: 2px solid #006633;
background: #0f2a1f;
color: #00ff99;
box-shadow: 0 0 8px rgba(0, 170, 68, 0.35);
}
.we-action-btn-edit.enabled:hover {
background: #163829;
border-color: #00aa66;
color: #ffffff;
}
.we-action-btn-edit.disabled {
border-color: #555566;
background: #1a1f26;
color: #778899;
opacity: 0.55;
}
/* Bright Eyeball Toggle */
.we-toggle-btn {
width: 26px; height: 26px;
border: 2px solid #334455;
border-radius: 5px;
background: #1a212b;
font-size: 14px;
color: #77ffaa;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all 0.15s ease;
}
.we-toggle-btn:hover {
background: #2a3f2f;
border-color: #00ff99;
color: #bbffdd;
transform: scale(1.1);
}
.we-del-btn {
width: 26px; height: 26px;
border: 2px solid #334455;
border-radius: 5px;
background: #1a212b;
color: #ff7777;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
}
.we-del-btn:hover {
background: #3a1f1f;
border-color: #ff5555;
color: #ffaaaa;
}
#we-lower {
border-top: 1px solid #1e2328;
padding: 5px 6px;
display: flex;
gap: 4px;
flex-shrink: 0;
background: #0e1114;
}
.we-ctrl-btn {
flex: 1;
padding: 5px 6px;
border: 1px solid #2a2f35;
border-radius: 4px;
background: #1a1e23;
color: #8a9098;
font-size: 11px;
font-weight: 600;
cursor: pointer;
}
.we-ctrl-btn:hover { background: #222830; color: #c8cdd4; }
#we-resize-handle {
position: absolute;
bottom: 0; right: 0;
width: 44px; height: 44px;
cursor: se-resize;
background: transparent;
z-index: 10;
touch-action: none;
}
#we-resize-handle::after {
content: '';
position: absolute;
bottom: 3px; right: 3px;
width: 6px; height: 6px;
border-right: 2px solid #3a4050;
border-bottom: 2px solid #3a4050;
border-radius: 1px;
}
/* Modals */
.we-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.65);
z-index: 100002;
display: flex;
align-items: center;
justify-content: center;
}
.we-modal {
background: #14181e;
border: 1px solid #2a3040;
border-radius: 8px;
box-shadow: 0 8px 40px rgba(0,0,0,0.8);
padding: 20px;
min-width: 280px;
max-width: 360px;
color: #c8cdd4;
}
.we-modal h3 {
margin: 0 0 14px;
font-size: 13px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #8098b8;
}
.we-modal label {
display: block;
font-size: 11px;
color: #6a7888;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 4px;
}
.we-modal input[type="text"] {
width: 100%;
box-sizing: border-box;
background: #1c2128;
border: 1px solid #2e3848;
border-radius: 4px;
color: #d8e0e8;
font-size: 12px;
padding: 7px 9px;
margin-bottom: 12px;
outline: none;
transition: border-color 0.15s;
}
.we-modal input[type="text"]:focus { border-color: #5080b0; }
.we-modal-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 4px;
}
.we-btn-primary {
padding: 7px 16px;
background: #1e3a5a;
border: 1px solid #3060a0;
border-radius: 4px;
color: #80b8f0;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.we-btn-primary:hover { background: #254570; }
.we-btn-cancel {
padding: 7px 14px;
background: #1a1e23;
border: 1px solid #2a2f35;
border-radius: 4px;
color: #6a7888;
font-size: 12px;
cursor: pointer;
}
.we-btn-cancel:hover { background: #22282e; }
.we-btn-danger {
padding: 7px 14px;
background: #2a1818;
border: 1px solid #6a2828;
border-radius: 4px;
color: #c06060;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.we-btn-danger:hover { background: #3a2020; }
#we-fab {
position: fixed;
z-index: 99998;
width: 44px;
height: 44px;
border-radius: 50%;
background: #0f2a1f;
border: 2px solid #006633;
box-shadow: 0 0 12px rgba(0, 170, 68, 0.4), 0 2px 8px rgba(0,0,0,0.6);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 10px;
transition: none;
user-select: none;
touch-action: manipulation; /* Helps click response on mobile */
-webkit-tap-highlight-color: rgba(0,0,0,0.1);
}
#we-fab.glow {
border-color: #00aa66;
box-shadow: 0 0 18px rgba(0, 204, 102, 0.6), 0 2px 8px rgba(0,0,0,0.6);
transform: scale(1.08);
}
#we-fab svg {
width: 100%;
height: 100%;
display: block;
}
#we-btn-settings {
background: none;
border: none;
cursor: pointer;
font-size: 14px;
padding: 0;
line-height: 1;
opacity: 0.6;
transition: opacity 0.15s, transform 0.3s;
flex-shrink: 0;
color: #9da6b0;
}
#we-btn-settings:hover {
opacity: 1;
transform: rotate(45deg);
}
#we-settings-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.65);
z-index: 100001;
display: flex;
align-items: center;
justify-content: center;
}
#we-settings-panel {
background: #14181e;
border: 1px solid #2a3040;
border-radius: 8px;
box-shadow: 0 8px 40px rgba(0,0,0,0.8);
width: 350px;
height: 500px;
display: flex;
flex-direction: column;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #c8cdd4;
overflow: hidden;
}
#we-settings-header {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid #2a3040;
background: #1a1e23;
flex-shrink: 0;
}
#we-settings-header h2 {
margin: 0;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #9da6b0;
flex: 1;
}
#we-settings-close {
background: none;
border: none;
color: #7a8490;
font-size: 16px;
cursor: pointer;
padding: 0;
line-height: 1;
}
#we-settings-close:hover { color: #c8cdd4; }
#we-settings-body {
display: flex;
flex: 1;
overflow: hidden;
}
#we-settings-lists {
width: 180px;
flex-shrink: 0;
border-right: 1px solid #2a3040;
display: flex;
flex-direction: column;
overflow: hidden;
}
#we-settings-lists-scroll {
flex: 1;
overflow-y: auto;
padding: 6px 0;
}
.we-settings-list-row {
display: flex;
align-items: center;
padding: 10px 10px;
cursor: pointer;
gap: 6px;
border-left: 3px solid transparent;
transition: background 0.1s;
}
.we-settings-list-row:hover { background: #1c2128; }
.we-settings-list-row.active {
background: #1c2128;
border-left-color: #006633;
}
.we-settings-list-name {
flex: 1;
font-size: 11px;
line-height: 1.5;
color: #c8cdd4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.we-settings-list-row.active .we-settings-list-name { color: #00ff99; }
.we-settings-list-edit {
background: none;
border: none;
color: #8a9098;
font-size: 12px;
cursor: pointer;
padding: 0 2px;
line-height: 1;
flex-shrink: 0;
}
.we-settings-list-edit:hover { color: #c8cdd4; }
.we-settings-list-delete {
background: none;
border: none;
color: #8a9098;
font-size: 13px;
cursor: pointer;
padding: 0 2px;
line-height: 1;
flex-shrink: 0;
}
.we-settings-list-delete:hover { color: #c96b6b; }
#we-settings-lists-footer {
padding: 6px 8px;
border-top: 1px solid #2a3040;
flex-shrink: 0;
}
#we-settings-items {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
#we-settings-items-header {
display: none;
}
#we-settings-items-scroll {
flex: 1;
overflow-y: auto;
padding: 6px 0;
}
.we-settings-item-row {
display: flex;
align-items: center;
padding: 10px 10px;
gap: 6px;
}
.we-settings-item-row:hover { background: #1a1e23; }
.we-settings-item-name {
flex: 1;
font-size: 11px;
line-height: 1.5;
color: #c8cdd4;
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.we-settings-item-name:hover { color: #00ff99; }
.we-settings-item-name.disabled { color: #6a7888; }
.we-settings-item-delete {
background: none;
border: none;
color: #8a9098;
font-size: 13px;
cursor: pointer;
padding: 0 2px;
line-height: 1;
flex-shrink: 0;
}
.we-settings-item-delete:hover { color: #c96b6b; }
#we-settings-items-footer {
padding: 6px 8px;
border-top: 1px solid #2a3040;
flex-shrink: 0;
}
#we-settings-footer {
border-top: 1px solid #2a3040;
padding: 10px 16px;
background: #0e1114;
flex-shrink: 0;
display: flex;
align-items: center;
gap: 10px;
}
.we-settings-toggle-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: #7a8490;
}
.we-settings-toggle {
width: 32px; height: 18px;
background: #2a2f35;
border-radius: 9px;
position: relative;
cursor: pointer;
border: none;
transition: background 0.2s;
flex-shrink: 0;
}
.we-settings-toggle.on { background: #006633; }
.we-settings-toggle::after {
content: '';
position: absolute;
width: 12px; height: 12px;
background: #c8cdd4;
border-radius: 50%;
top: 3px; left: 3px;
transition: left 0.2s;
}
.we-settings-toggle.on::after { left: 17px; }
.we-settings-add-btn {
width: 100%;
padding: 5px;
background: #1a1e23;
border: 1px dashed #2a3040;
border-radius: 4px;
color: #4a5568;
font-size: 11px;
cursor: pointer;
text-align: center;
transition: color 0.1s, border-color 0.1s;
}
.we-settings-add-btn:hover { color: #c8cdd4; border-color: #4a5568; }
#we-btn-lock {
background: none;
border: none;
cursor: pointer;
font-size: 12px;
padding: 0;
line-height: 1;
opacity: 0.6;
transition: opacity 0.15s;
flex-shrink: 0;
}
.we-settings-item-drag {
color: #3a4555;
font-size: 13px;
cursor: grab;
padding: 0 2px;
flex-shrink: 0;
user-select: none;
}
.we-settings-item-drag:active { cursor: grabbing; }
.we-settings-item-row.drag-over-top { border-top: 2px solid #5080b0; }
.we-settings-item-row.drag-over-bot { border-bottom: 2px solid #5080b0; }
#we-btn-lock:hover { opacity: 1; }
`;
document.head.appendChild(style);
// ─── Panel Construction ───────────────────────────────────────────────────
const panel = document.createElement('div');
panel.id = 'we-panel';
panel.style.left = state.pos.x + 'px';
panel.style.top = state.pos.y + 'px';
panel.style.width = state.size.w + 'px';
panel.style.height = state.size.h + 'px';
panel.innerHTML = `
<div id="we-titlebar">
<div id="we-title-icon"></div>
<span id="we-title-text">OpsCheck</span>
<button id="we-btn-settings" title="Settings">⚙</button>
<button id="we-btn-lock">🔓</button>
</div>
<div id="we-selector">
<select id="we-sel-dropdown"></select>
</div>
<div id="we-upper"></div>
<div id="we-lower">
<button class="we-ctrl-btn" id="we-btn-reset">Reset</button>
</div>
<div id="we-resize-handle"></div>
`;
document.body.appendChild(panel);
const resizeHandle = panel.querySelector('#we-resize-handle');
function updateResizeHandle() {}
const fab = document.createElement('button');
fab.id = 'we-fab';
fab.innerHTML = `<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 13L9 18L20 7" stroke="#00ff99" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
document.body.appendChild(fab);
function applyFabPos() {
const size = 44;
const margin = 15;
let x = state.fabPos.x;
let y = state.fabPos.y;
const maxX = window.innerWidth - size - margin;
const maxY = window.innerHeight - size - margin;
x = Math.max(margin, Math.min(x, maxX));
y = Math.max(margin, Math.min(y, maxY));
fab.style.left = x + 'px';
fab.style.top = y + 'px';
// Do NOT save state here during resize
}
function applyPanelVisible() {
panel.style.display = state.panelVisible ? '' : 'none';
}
applyFabPos();
applyPanelVisible();
// Hide panel on attack pages regardless
if (state.hideOnAttack && /[?&]sid=attack/.test(window.location.search)) {
panel.style.display = 'none';
}
const upper = panel.querySelector('#we-upper');
const btnReset = panel.querySelector('#we-btn-reset');
const btnLock = panel.querySelector('#we-btn-lock');
const btnSettings = panel.querySelector('#we-btn-settings');
const titlebar = panel.querySelector('#we-titlebar');
const selDropdown = panel.querySelector('#we-sel-dropdown');
let resizing = false, resStartX, resStartY, resStartW, resStartH;
function renderSelector() {
selDropdown.innerHTML = '';
// Sort lists alphabetically, keeping "New Checklist" at bottom
const sorted = [...state.lists].sort((a, b) => a.name.localeCompare(b.name));
sorted.forEach(list => {
if (!list.enabled) return;
const opt = document.createElement('option');
opt.value = list.id;
opt.textContent = list.name;
if (list.id === state.activeListId) opt.selected = true;
selDropdown.appendChild(opt);
});
}
// ─── Render Upper ─────────────────────────────────────────────────────────
function renderUpper() {
renderSelector();
upper.innerHTML = '';
if (!activeList()) {
const hint = document.createElement('div');
hint.className = 'we-empty-hint';
hint.textContent = 'No checklists enabled. Open Settings to enable one.';
upper.appendChild(hint);
return;
}
const visibleButtons = activeList().buttons.filter(b => b.enabled);
if (!activeList().buttons.length) {
const hint = document.createElement('div');
hint.className = 'we-empty-hint';
hint.textContent = 'No items yet. Open Settings to add one.';
upper.appendChild(hint);
return;
}
if (!visibleButtons.length) {
const hint = document.createElement('div');
hint.className = 'we-empty-hint';
hint.textContent = 'No visible items. Open Settings to enable some.';
upper.appendChild(hint);
return;
}
activeList().buttons.forEach((btn, idx) => {
if (!btn.enabled) return;
const row = document.createElement('div');
row.className = 'we-row';
const actionBtn = document.createElement('button');
actionBtn.className = 'we-action-btn';
const nameSpan = document.createElement('span');
nameSpan.textContent = btn.name;
nameSpan.style.cssText = 'overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0;';
actionBtn.appendChild(nameSpan);
if (btn.url) actionBtn.classList.add('has-link');
if (btn.done) {
actionBtn.disabled = true;
actionBtn.style.pointerEvents = 'none';
row.style.cursor = 'pointer';
row.addEventListener('click', () => toggleDone(idx));
} else {
actionBtn.disabled = false;
actionBtn.addEventListener('click', () => handleActionClick(idx));
}
row.appendChild(actionBtn);
upper.appendChild(row);
});
}
// ─── Actions ──────────────────────────────────────────────────────────────
function handleActionClick(idx) {
const btn = activeList().buttons[idx];
if (!btn) return;
if (btn.done) { toggleDone(idx); return; }
if (!btn.url) {
btn.done = true;
saveState();
renderUpper();
return;
}
try {
const targetUrl = new URL(btn.url, window.location.origin);
const current = new URL(window.location.href);
const onPage = current.origin + current.pathname + current.search ===
targetUrl.origin + targetUrl.pathname + targetUrl.search;
btn.done = true;
saveState();
renderUpper();
if (!onPage) window.location.href = btn.url;
} catch (e) {
btn.done = true;
saveState();
renderUpper();
window.location.href = btn.url;
}
}
function toggleDone(idx) {
activeList().buttons[idx].done = !activeList().buttons[idx].done;
saveState();
renderUpper();
}
// ─── Modals ───────────────────────────────────────────────────────────────
function makeOverlay() {
const overlay = document.createElement('div');
overlay.className = 'we-overlay';
return overlay;
}
function escHtml(str) {
return String(str).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
}
function showAddModal(listId, callback) {
const targetList = listId ? state.lists.find(l => l.id === listId) : activeList();
const overlay = makeOverlay();
const modal = document.createElement('div');
modal.className = 'we-modal';
modal.innerHTML = `
<h3>Add Button</h3>
<label>Name</label>
<input type="text" autocomplete="off" id="we-input-name" placeholder="e.g. Attack Target" maxlength="40" />
<label>URL</label>
<input type="text" autocomplete="off" id="we-input-url" placeholder="https://www.torn.com/... (leave blank for no navigation)" />
<div class="we-modal-actions">
<button class="we-btn-cancel" id="we-modal-cancel">Cancel</button>
<button class="we-btn-primary" id="we-modal-save">Add</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
const nameInput = modal.querySelector('#we-input-name');
const urlInput = modal.querySelector('#we-input-url');
nameInput.focus();
modal.querySelector('#we-modal-cancel').addEventListener('click', () => overlay.remove());
modal.querySelector('#we-modal-save').addEventListener('click', () => {
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (!name) return;
targetList.buttons.push({ name, url, done: false, enabled: true });
saveState();
if (callback) callback(); else renderUpper();
overlay.remove();
});
[nameInput, urlInput].forEach(el => el.addEventListener('keydown', e => {
if (e.key === 'Enter') modal.querySelector('#we-modal-save').click();
if (e.key === 'Escape') overlay.remove();
}));
}
function showEditModal(idx, listId, callback) {
const targetList = listId ? state.lists.find(l => l.id === listId) : activeList();
const btn = targetList.buttons[idx];
const overlay = makeOverlay();
const modal = document.createElement('div');
modal.className = 'we-modal';
modal.innerHTML = `
<h3>Edit Button</h3>
<label>Name</label>
<input type="text" autocomplete="off" id="we-input-name" value="${escHtml(btn.name)}" maxlength="40" />
<label>URL</label>
<input type="text" autocomplete="off" id="we-input-url" value="${escHtml(btn.url || '')}" placeholder="https://www.torn.com/... (leave blank for no navigation)" />
<div class="we-modal-actions">
<button class="we-btn-cancel" id="we-modal-cancel">Cancel</button>
<button class="we-btn-primary" id="we-modal-save">Save</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
const nameInput = modal.querySelector('#we-input-name');
const urlInput = modal.querySelector('#we-input-url');
nameInput.focus();
nameInput.select();
modal.querySelector('#we-modal-cancel').addEventListener('click', () => overlay.remove());
modal.querySelector('#we-modal-save').addEventListener('click', () => {
const name = nameInput.value.trim();
const url = urlInput.value.trim();
if (!name) return;
targetList.buttons[idx].name = name;
targetList.buttons[idx].url = url;
saveState();
if (callback) callback(); else renderUpper();
overlay.remove();
});
[nameInput, urlInput].forEach(el => el.addEventListener('keydown', e => {
if (e.key === 'Enter') modal.querySelector('#we-modal-save').click();
if (e.key === 'Escape') overlay.remove();
}));
}
function showDeleteConfirm(idx, listId, callback) {
const targetList = listId ? state.lists.find(l => l.id === listId) : activeList();
const btn = targetList.buttons[idx];
const overlay = makeOverlay();
const modal = document.createElement('div');
modal.className = 'we-modal';
modal.innerHTML = `
<h3>Delete Button</h3>
<p class="we-confirm-text">Delete "<strong>${escHtml(btn.name)}</strong>"? This cannot be undone.</p>
<div class="we-modal-actions">
<button class="we-btn-cancel" id="we-modal-cancel">Cancel</button>
<button class="we-btn-danger" id="we-modal-delete">Delete</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
modal.querySelector('#we-modal-cancel').addEventListener('click', () => overlay.remove());
modal.querySelector('#we-modal-delete').addEventListener('click', () => {
targetList.buttons.splice(idx, 1);
saveState();
if (callback) callback(); else renderUpper();
overlay.remove();
});
}
function generateId() {
return 'list-' + Date.now().toString(36);
}
function showNewListModal(callback, noActivate) {
const overlay = makeOverlay();
const modal = document.createElement('div');
modal.className = 'we-modal';
modal.innerHTML = `
<h3>New Checklist</h3>
<label>Name</label>
<input type="text" autocomplete="off" id="we-input-name" placeholder="e.g. Faction War" maxlength="40" />
<div class="we-modal-actions">
<button class="we-btn-cancel" id="we-modal-cancel">Cancel</button>
<button class="we-btn-primary" id="we-modal-save">Create</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
const nameInput = modal.querySelector('#we-input-name');
nameInput.focus();
const doSave = () => {
const name = nameInput.value.trim();
if (!name) { nameInput.style.borderColor = '#a04040'; return; }
const newList = { id: generateId(), name, buttons: [], enabled: true };
state.lists.push(newList);
if (!noActivate) state.activeListId = newList.id;
saveState();
if (callback) callback(newList.id); else renderUpper();
overlay.remove();
};
modal.querySelector('#we-modal-cancel').addEventListener('click', () => {
// Revert dropdown to previous selection
renderSelector();
overlay.remove();
});
modal.querySelector('#we-modal-save').addEventListener('click', doSave);
nameInput.addEventListener('keydown', e => {
if (e.key === 'Enter') doSave();
if (e.key === 'Escape') { renderSelector(); overlay.remove(); }
});
}
function showRenameListModal(listId, callback) {
const list = listId ? state.lists.find(l => l.id === listId) : activeList();
const overlay = makeOverlay();
const modal = document.createElement('div');
modal.className = 'we-modal';
modal.innerHTML = `
<h3>Rename Checklist</h3>
<label>Name</label>
<input type="text" autocomplete="off" id="we-input-name" value="${escHtml(list.name)}" maxlength="40" />
<div class="we-modal-actions">
<button class="we-btn-cancel" id="we-modal-cancel">Cancel</button>
<button class="we-btn-primary" id="we-modal-save">Save</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
const nameInput = modal.querySelector('#we-input-name');
nameInput.focus();
nameInput.select();
const doSave = () => {
const name = nameInput.value.trim();
if (!name) { nameInput.style.borderColor = '#a04040'; return; }
list.name = name;
saveState();
if (callback) callback(); else renderUpper();
overlay.remove();
};
modal.querySelector('#we-modal-cancel').addEventListener('click', () => overlay.remove());
modal.querySelector('#we-modal-save').addEventListener('click', doSave);
nameInput.addEventListener('keydown', e => {
if (e.key === 'Enter') doSave();
if (e.key === 'Escape') overlay.remove();
});
}
function showDeleteListConfirm(listId, callback) {
const list = listId ? state.lists.find(l => l.id === listId) : activeList();
const overlay = makeOverlay();
const modal = document.createElement('div');
modal.className = 'we-modal';
modal.innerHTML = `
<h3>Delete Checklist</h3>
<p class="we-confirm-text">Delete "<strong>${escHtml(list.name)}</strong>" and all its buttons? This cannot be undone.</p>
<div class="we-modal-actions">
<button class="we-btn-cancel" id="we-modal-cancel">Cancel</button>
<button class="we-btn-danger" id="we-modal-delete">Delete</button>
</div>
`;
overlay.appendChild(modal);
document.body.appendChild(overlay);
modal.querySelector('#we-modal-cancel').addEventListener('click', () => overlay.remove());
modal.querySelector('#we-modal-delete').addEventListener('click', () => {
state.lists = state.lists.filter(l => l.id !== list.id);
if (state.activeListId === list.id) state.activeListId = state.lists[0].id;
saveState();
if (callback) callback(); else renderUpper();
overlay.remove();
});
}
let settingsSelectedListId = null;
function showSettingsPanel() {
settingsSelectedListId = state.activeListId;
const overlay = document.createElement('div');
overlay.id = 'we-settings-overlay';
document.body.appendChild(overlay);
const sp = document.createElement('div');
sp.id = 'we-settings-panel';
overlay.appendChild(sp);
sp.innerHTML = `
<div id="we-settings-header">
<h2>⚙ Settings</h2>
<button id="we-settings-close">✕</button>
</div>
<div id="we-settings-body">
<div id="we-settings-lists">
<div id="we-settings-lists-scroll"></div>
<div id="we-settings-lists-footer">
<button class="we-settings-add-btn" id="we-settings-new-list">+ New Checklist</button>
</div>
</div>
<div id="we-settings-items">
<div id="we-settings-items-header" id="we-settings-items-title"></div>
<div id="we-settings-items-scroll"></div>
<div id="we-settings-items-footer">
<button class="we-settings-add-btn" id="we-settings-new-item">+ Add Item</button>
</div>
</div>
</div>
<div id="we-settings-footer">
<div class="we-settings-toggle-row">
<button class="we-settings-toggle ${state.hideOnAttack ? 'on' : ''}" id="we-settings-attack-toggle"></button>
<span>Hide checklist on attack pages</span>
</div>
</div>
`;
function renderSettingsLists() {
const scroll = sp.querySelector('#we-settings-lists-scroll');
scroll.innerHTML = '';
const sorted = [...state.lists].sort((a, b) => a.name.localeCompare(b.name));
sorted.forEach(list => {
const row = document.createElement('div');
row.className = 'we-settings-list-row' + (list.id === settingsSelectedListId ? ' active' : '');
const name = document.createElement('span');
name.className = 'we-settings-list-name';
name.textContent = list.name;
name.addEventListener('click', () => {
settingsSelectedListId = list.id;
renderSettingsLists();
renderSettingsItems();
});
const eyeBtn = document.createElement('button');
eyeBtn.className = 'we-settings-list-edit';
eyeBtn.textContent = list.enabled ? '👁' : '🚫';
eyeBtn.title = list.enabled ? 'Hide from dropdown' : 'Show in dropdown';
eyeBtn.addEventListener('click', e => {
e.stopPropagation();
list.enabled = !list.enabled;
saveState();
renderSettingsLists();
renderUpper();
});
const editBtn = document.createElement('button');
editBtn.className = 'we-settings-list-edit';
editBtn.textContent = '✎';
editBtn.title = 'Rename';
editBtn.addEventListener('click', e => {
e.stopPropagation();
showRenameListModal(list.id, () => {
renderSettingsLists();
renderSettingsItems();
renderUpper();
});
});
const delBtn = document.createElement('button');
delBtn.className = 'we-settings-list-delete';
delBtn.textContent = '🗑';
delBtn.title = 'Delete';
delBtn.addEventListener('click', e => {
e.stopPropagation();
showDeleteListConfirm(list.id, () => {
settingsSelectedListId = state.activeListId;
renderSettingsLists();
renderSettingsItems();
renderUpper();
});
});
row.appendChild(name);
row.appendChild(eyeBtn);
row.appendChild(editBtn);
row.appendChild(delBtn);
scroll.appendChild(row);
});
}
function renderSettingsItems() {
const scroll = sp.querySelector('#we-settings-items-scroll');
scroll.innerHTML = '';
if (!settingsSelectedListId) return;
const list = state.lists.find(l => l.id === settingsSelectedListId);
if (!list) return;
list.buttons.forEach((btn, idx) => {
const row = document.createElement('div');
row.className = 'we-settings-item-row';
const dragHandle = document.createElement('span');
dragHandle.className = 'we-settings-item-drag';
dragHandle.textContent = '⠿';
row.setAttribute('draggable', 'true');
row.dataset.idx = idx;
// Desktop drag
row.addEventListener('dragstart', e => {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', idx);
setTimeout(() => row.style.opacity = '0.4', 0);
});
row.addEventListener('dragend', () => {
row.style.opacity = '';
sp.querySelectorAll('.we-settings-item-row').forEach(r => r.classList.remove('drag-over-top', 'drag-over-bot'));
});
row.addEventListener('dragover', e => {
e.preventDefault();
const rect = row.getBoundingClientRect();
const mid = rect.top + rect.height / 2;
row.classList.toggle('drag-over-top', e.clientY < mid);
row.classList.toggle('drag-over-bot', e.clientY >= mid);
});
row.addEventListener('dragleave', () => row.classList.remove('drag-over-top', 'drag-over-bot'));
row.addEventListener('drop', e => {
e.preventDefault();
row.classList.remove('drag-over-top', 'drag-over-bot');
const fromIdx = parseInt(e.dataTransfer.getData('text/plain'));
const rect = row.getBoundingClientRect();
const insertBefore = e.clientY < rect.top + rect.height / 2;
let toIdx = insertBefore ? idx : idx + 1;
if (fromIdx === toIdx || fromIdx === toIdx - 1) return;
const item = list.buttons.splice(fromIdx, 1)[0];
if (fromIdx < toIdx) toIdx--;
list.buttons.splice(toIdx, 0, item);
saveState();
renderSettingsItems();
renderUpper();
});
// Touch drag
dragHandle.addEventListener('touchstart', e => {
const touch = e.touches[0];
let touchDragging = true;
const startY = touch.clientY;
const rowHeight = row.getBoundingClientRect().height;
row.style.opacity = '0.4';
const onMove = e => {
if (!touchDragging) return;
const t = e.touches[0];
const rows = [...sp.querySelectorAll('.we-settings-item-row')];
rows.forEach(r => r.classList.remove('drag-over-top', 'drag-over-bot'));
const target = rows.find(r => {
const rect = r.getBoundingClientRect();
return t.clientY >= rect.top && t.clientY <= rect.bottom;
});
if (target && target !== row) {
const rect = target.getBoundingClientRect();
const mid = rect.top + rect.height / 2;
target.classList.toggle('drag-over-top', t.clientY < mid);
target.classList.toggle('drag-over-bot', t.clientY >= mid);
}
e.preventDefault();
};
const onEnd = e => {
touchDragging = false;
row.style.opacity = '';
const rows = [...sp.querySelectorAll('.we-settings-item-row')];
const overTop = sp.querySelector('.we-settings-item-row.drag-over-top');
const overBot = sp.querySelector('.we-settings-item-row.drag-over-bot');
rows.forEach(r => r.classList.remove('drag-over-top', 'drag-over-bot'));
const target = overTop || overBot;
if (!target) return;
const toIdx2 = parseInt(target.dataset.idx);
const insertBefore = !!overTop;
let toIdx = insertBefore ? toIdx2 : toIdx2 + 1;
if (idx === toIdx || idx === toIdx - 1) return;
const item = list.buttons.splice(idx, 1)[0];
if (idx < toIdx) toIdx--;
list.buttons.splice(toIdx, 0, item);
saveState();
renderSettingsItems();
renderUpper();
document.removeEventListener('touchmove', onMove);
document.removeEventListener('touchend', onEnd);
};
document.addEventListener('touchmove', onMove, { passive: false });
document.addEventListener('touchend', onEnd);
e.stopPropagation();
}, { passive: true });
const toggleBtn = document.createElement('button');
toggleBtn.className = 'we-settings-list-edit';
toggleBtn.textContent = btn.enabled ? '👁' : '🚫';
toggleBtn.title = btn.enabled ? 'Hide' : 'Show';
toggleBtn.addEventListener('click', () => {
btn.enabled = !btn.enabled;
saveState();
renderSettingsItems();
renderUpper();
});
const nameEl = document.createElement('span');
nameEl.className = 'we-settings-item-name' + (btn.enabled ? '' : ' disabled');
nameEl.textContent = btn.name + (btn.url ? ' 🔗' : '');
nameEl.title = 'Click to edit';
nameEl.addEventListener('click', () => {
showEditModal(idx, list.id, () => {
renderSettingsItems();
renderUpper();
});
});
const delBtn = document.createElement('button');
delBtn.className = 'we-settings-item-delete';
delBtn.textContent = '🗑';
delBtn.title = 'Delete';
delBtn.addEventListener('click', () => {
showDeleteConfirm(idx, list.id, () => {
renderSettingsItems();
renderUpper();
});
});
row.appendChild(dragHandle);
row.appendChild(toggleBtn);
row.appendChild(nameEl);
row.appendChild(delBtn);
scroll.appendChild(row);
});
}
sp.querySelector('#we-settings-close').addEventListener('click', () => overlay.remove());
sp.querySelector('#we-settings-new-list').addEventListener('click', () => {
showNewListModal(() => {
renderSettingsLists();
renderUpper();
}, true);
});
sp.querySelector('#we-settings-new-item').addEventListener('click', () => {
if (!settingsSelectedListId) return;
showAddModal(settingsSelectedListId, () => {
renderSettingsItems();
renderUpper();
});
});
sp.querySelector('#we-settings-attack-toggle').addEventListener('click', e => {
state.hideOnAttack = !state.hideOnAttack;
e.target.classList.toggle('on', state.hideOnAttack);
saveState();
});
renderSettingsLists();
renderSettingsItems();
}
// ─── Control Buttons ──────────────────────────────────────────────────────
btnReset.addEventListener('click', () => {
activeList().buttons.forEach(b => b.done = false);
saveState();
renderUpper();
});
btnSettings.addEventListener('click', () => showSettingsPanel());
selDropdown.addEventListener('change', () => {
state.activeListId = selDropdown.value;
saveState();
renderUpper();
});
btnLock.addEventListener('click', () => {
state.locked = !state.locked;
saveState();
btnLock.textContent = state.locked ? '🔒' : '🔓';
titlebar.style.cursor = state.locked ? 'default' : 'grab';
resizeHandle.style.visibility = state.locked ? 'hidden' : 'visible';
updateResizeHandle();
});
// Initial Setup
btnLock.textContent = state.locked ? '🔒' : '🔓';
titlebar.style.cursor = state.locked ? 'default' : 'grab';
resizeHandle.style.visibility = state.locked ? 'hidden' : 'visible';
updateResizeHandle();
// ─── FAB Drag & Tap (Torn PDA + Desktop Fix v8) ───────────────────────────
let fabDragged = false;
let fabDragging = false, fabStartX, fabStartY, fabOrigX, fabOrigY;
let lastTapTime = 0;
let isDragging = false; // Global flag to suppress click after drag
function onFabDragStart(clientX, clientY) {
fabDragging = true;
fabDragged = false;
isDragging = false;
fabStartX = clientX;
fabStartY = clientY;
fabOrigX = fab.offsetLeft;
fabOrigY = fab.offsetTop;
}
function onFabDragMove(clientX, clientY) {
if (!fabDragging) return;
const dx = clientX - fabStartX;
const dy = clientY - fabStartY;
if (Math.abs(dx) > 6 || Math.abs(dy) > 6) {
fabDragged = true;
isDragging = true;
}
const size = 44, margin = 10;
const nx = Math.max(margin, Math.min(fabOrigX + dx, window.innerWidth - size - margin));
const ny = Math.max(margin, Math.min(fabOrigY + dy, window.innerHeight - size - margin));
fab.style.left = nx + 'px';
fab.style.top = ny + 'px';
state.fabPos = { x: nx, y: ny };
}
function onFabDragEnd() {
fabDragging = false;
if (fabDragged) {
saveState();
}
// Do not reset isDragging here - let click handler handle it
}
function handleFabTap() {
const now = Date.now();
if (now - lastTapTime < 250) return;
lastTapTime = now;
if (fabDragged || isDragging) {
fabDragged = false;
isDragging = false;
return;
}
state.panelVisible = !state.panelVisible;
saveState();
applyPanelVisible();
if (state.hideOnAttack && /[?&]sid=attack/.test(window.location.search)) {
panel.style.display = 'none';
}
}
// === LISTENERS ===
fab.addEventListener('click', (e) => {
// This is the critical part for desktop
if (isDragging) {
isDragging = false;
fabDragged = false;
return;
}
handleFabTap();
});
fab.addEventListener('mousedown', e => {
if (e.button !== 0) return;
onFabDragStart(e.clientX, e.clientY);
e.preventDefault();
});
fab.addEventListener('touchstart', e => {
const t = e.touches[0];
onFabDragStart(t.clientX, t.clientY);
fab.classList.add('glow');
e.preventDefault();
}, { passive: false });
fab.addEventListener('touchmove', e => {
if (!fabDragging) return;
const t = e.touches[0];
onFabDragMove(t.clientX, t.clientY);
e.preventDefault();
updateResizeHandle();
}, { passive: false });
fab.addEventListener('touchend', e => {
fab.classList.remove('glow');
setTimeout(() => {
handleFabTap();
onFabDragEnd();
}, 30);
}, { passive: true });
document.addEventListener('mousemove', e => {
onFabDragMove(e.clientX, e.clientY);
updateResizeHandle();
});
document.addEventListener('mouseup', () => {
onFabDragEnd();
// Small delay to let click event see the flag
setTimeout(() => {
isDragging = false;
}, 50);
});
document.addEventListener('touchend', onFabDragEnd);
// ─── Drag ─────────────────────────────────────────────────────────────────
let dragging = false, dragStartX, dragStartY, panelStartX, panelStartY;
titlebar.addEventListener('mousedown', e => {
if (e.button !== 0 || state.locked) return;
dragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
panelStartX = panel.offsetLeft;
panelStartY = panel.offsetTop;
e.preventDefault();
});
titlebar.addEventListener('touchstart', e => {
if (state.locked) return;
if (e.target.closest('button')) return;
const t = e.touches[0];
dragging = true;
dragStartX = t.clientX;
dragStartY = t.clientY;
panelStartX = panel.offsetLeft;
panelStartY = panel.offsetTop;
e.preventDefault();
}, { passive: false });
document.addEventListener('mousemove', e => {
if (!dragging) return;
const nx = panelStartX + (e.clientX - dragStartX);
const ny = panelStartY + (e.clientY - dragStartY);
panel.style.left = Math.max(0, Math.min(nx, window.innerWidth - panel.offsetWidth)) + 'px';
panel.style.top = Math.max(0, Math.min(ny, window.innerHeight - panel.offsetHeight)) + 'px';
updateResizeHandle();
});
document.addEventListener('touchmove', e => {
if (!dragging && !resizing) return;
if (dragging) {
const t = e.touches[0];
const nx = panelStartX + (t.clientX - dragStartX);
const ny = panelStartY + (t.clientY - dragStartY);
panel.style.left = Math.max(0, Math.min(nx, window.innerWidth - panel.offsetWidth)) + 'px';
panel.style.top = Math.max(0, Math.min(ny, window.innerHeight - panel.offsetHeight)) + 'px';
updateResizeHandle();
}
if (resizing) {
const t = e.touches[0];
const nw = Math.max(MIN_WIDTH, resStartW + (t.clientX - resStartX));
const nh = Math.max(120, resStartH + (t.clientY - resStartY));
panel.style.width = nw + 'px';
panel.style.height = nh + 'px';
}
e.preventDefault();
}, { passive: false });
document.addEventListener('mouseup', () => {
if (dragging) {
dragging = false;
state.pos = { x: panel.offsetLeft, y: panel.offsetTop };
saveState();
}
if (resizing) {
resizing = false;
state.size = { w: panel.offsetWidth, h: panel.offsetHeight };
saveState();
}
});
document.addEventListener('touchend', () => {
if (dragging) {
dragging = false;
state.pos = { x: panel.offsetLeft, y: panel.offsetTop };
saveState();
}
if (resizing) {
resizing = false;
state.size = { w: panel.offsetWidth, h: panel.offsetHeight };
saveState();
}
});
// ─── Resize ───────────────────────────────────────────────────────────────
resizeHandle.addEventListener('mousedown', e => {
if (e.button !== 0 || state.locked) return;
resizing = true;
resStartX = e.clientX;
resStartY = e.clientY;
resStartW = panel.offsetWidth;
resStartH = panel.offsetHeight;
e.preventDefault();
e.stopPropagation();
});
resizeHandle.addEventListener('touchstart', e => {
if (state.locked) return;
const t = e.touches[0];
resizing = true;
resStartX = t.clientX;
resStartY = t.clientY;
resStartW = panel.offsetWidth;
resStartH = panel.offsetHeight;
e.preventDefault();
}, { passive: false });
document.addEventListener('mousemove', e => {
if (!resizing) return;
const nw = Math.max(MIN_WIDTH, resStartW + (e.clientX - resStartX));
const nh = Math.max(120, resStartH + (e.clientY - resStartY));
panel.style.width = nw + 'px';
panel.style.height = nh + 'px';
updateResizeHandle();
});
// ─── Init ─────────────────────────────────────────────────────────────────
function initDoneState() {
renderUpper();
updateResizeHandle();
}
// ─── Window Resize Handler (Preserve Original Position) ───────────────────
window.addEventListener('resize', () => {
// --- Main Panel ---
const savedLeft = state.pos.x;
const savedTop = state.pos.y;
const panelW = panel.offsetWidth;
const panelH = panel.offsetHeight;
const maxLeft = Math.max(0, window.innerWidth - panelW - 20);
const maxTop = Math.max(0, window.innerHeight - panelH - 20);
// Only clamp for current display, do NOT overwrite saved position
let displayLeft = Math.max(0, Math.min(savedLeft, maxLeft));
let displayTop = Math.max(0, Math.min(savedTop, maxTop));
panel.style.left = displayLeft + 'px';
panel.style.top = displayTop + 'px';
updateResizeHandle();
// --- FAB ---
applyFabPos();
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDoneState);
} else if (document.body) {
initDoneState();
} else {
window.addEventListener('load', initDoneState);
}
})();