Multi-checklist action panel for Torn.com
// ==UserScript==
// @name OpsCheck
// @version 2.0.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
};
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
};
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();
function activeList() {
return state.lists.find(l => l.id === state.activeListId) || state.lists[0];
}
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;
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: 28px; height: 28px;
cursor: se-resize;
background: transparent;
z-index: 10;
}
#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: 100000;
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; }
`;
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.minimized ? 'auto' : state.size.h + 'px';
if (state.minimized) panel.classList.add('minimized');
panel.innerHTML = `
<div id="we-titlebar">
<div id="we-title-icon"></div>
<span id="we-title-text">Ops Check</span>
<button id="we-btn-minimize">—</button>
<button id="we-btn-lock">🔓</button>
</div>
<div id="we-selector">
<button class="we-sel-btn danger" id="we-sel-delete" title="Delete list">✕</button>
<button class="we-sel-btn" id="we-sel-rename" title="Rename list">✎</button>
<select id="we-sel-dropdown"></select>
</div>
<div id="we-upper"></div>
<div id="we-lower">
<button class="we-ctrl-btn" id="we-btn-edit">Edit</button>
<button class="we-ctrl-btn" id="we-btn-reset">Reset</button>
</div>
<div id="we-resize-handle"></div>
`;
document.body.appendChild(panel);
if (/[?&]sid=attack/.test(window.location.search)) {
panel.style.display = 'none';
}
const upper = panel.querySelector('#we-upper');
const btnEdit = panel.querySelector('#we-btn-edit');
const btnReset = panel.querySelector('#we-btn-reset');
const btnLock = panel.querySelector('#we-btn-lock');
const btnMinimize = panel.querySelector('#we-btn-minimize');
const titlebar = panel.querySelector('#we-titlebar');
const resizeHandle = panel.querySelector('#we-resize-handle');
const selDropdown = panel.querySelector('#we-sel-dropdown');
const selDelete = panel.querySelector('#we-sel-delete');
const selRename = panel.querySelector('#we-sel-rename');
let editMode = false;
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 => {
const opt = document.createElement('option');
opt.value = list.id;
opt.textContent = list.name;
if (list.id === state.activeListId) opt.selected = true;
selDropdown.appendChild(opt);
});
// "New Checklist" at bottom
const newOpt = document.createElement('option');
newOpt.value = '__new__';
newOpt.textContent = '+ New Checklist';
selDropdown.appendChild(newOpt);
}
// ─── Render Upper ─────────────────────────────────────────────────────────
function renderUpper() {
renderSelector();
upper.innerHTML = '';
if (!activeList().buttons.length) {
const hint = document.createElement('div');
hint.className = 'we-empty-hint';
hint.textContent = 'No buttons — enter Edit mode to add one';
upper.appendChild(hint);
return;
}
activeList().buttons.forEach((btn, idx) => {
if (!editMode && !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 (editMode) {
// Edit Mode
actionBtn.classList.add('we-action-btn-edit');
if (btn.enabled) actionBtn.classList.add('enabled');
else actionBtn.classList.add('disabled');
actionBtn.addEventListener('click', () => showEditModal(idx));
} else {
// Normal Mode
if (btn.done) {
// COMPLETED button - clicking anywhere on row re-enables it
actionBtn.disabled = true; // keep visual "done" look
actionBtn.style.pointerEvents = 'none'; // prevent button itself from stealing clicks
row.style.cursor = 'pointer';
row.addEventListener('click', () => toggleDone(idx));
} else {
// Active button
actionBtn.disabled = false;
actionBtn.addEventListener('click', () => handleActionClick(idx));
}
}
if (editMode && !btn.enabled) row.style.opacity = '0.45';
if (editMode) {
row.setAttribute('draggable', 'true');
row.dataset.idx = idx;
// Drag & Drop (unchanged)
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 = '';
document.querySelectorAll('.we-row').forEach(r => r.classList.remove('drag-over-top', 'drag-over-bot'));
});
row.addEventListener('dragover', e => {
e.preventDefault();
const rect = row.getBoundingClientRect();
const midY = rect.top + rect.height / 2;
row.classList.toggle('drag-over-top', e.clientY < midY);
row.classList.toggle('drag-over-bot', e.clientY >= midY);
});
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 = activeList().buttons.splice(fromIdx, 1)[0];
if (fromIdx < toIdx) toIdx--;
activeList().buttons.splice(toIdx, 0, item);
saveState();
renderUpper();
});
const toggleBtn = document.createElement('button');
toggleBtn.className = 'we-toggle-btn';
toggleBtn.title = btn.enabled ? 'Hide from panel' : 'Show on panel';
toggleBtn.textContent = btn.enabled ? '👁' : '🚫';
toggleBtn.addEventListener('click', (e) => {
e.stopPropagation();
activeList().buttons[idx].enabled = !activeList().buttons[idx].enabled;
saveState();
renderUpper();
});
const delBtn = document.createElement('button');
delBtn.className = 'we-del-btn';
delBtn.title = 'Delete';
delBtn.textContent = '✕';
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
showDeleteConfirm(idx);
});
row.appendChild(toggleBtn);
row.appendChild(actionBtn);
row.appendChild(delBtn);
} else {
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() {
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;
activeList().buttons.push({ name, url, done: false, enabled: true });
saveState();
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();
}));
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
}
function showEditModal(idx) {
const btn = activeList().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;
activeList().buttons[idx].name = name;
activeList().buttons[idx].url = url;
saveState();
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();
}));
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
}
function showDeleteConfirm(idx) {
const btn = activeList().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', () => {
activeList().buttons.splice(idx, 1);
saveState();
renderUpper();
overlay.remove();
});
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
}
function generateId() {
return 'list-' + Date.now().toString(36);
}
function showNewListModal() {
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: [] };
state.lists.push(newList);
state.activeListId = newList.id;
saveState();
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(); }
});
overlay.addEventListener('click', e => { if (e.target === overlay) { renderSelector(); overlay.remove(); } });
}
function showRenameListModal() {
const list = 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();
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();
});
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
}
function showDeleteListConfirm() {
const list = activeList();
if (state.lists.length === 1) {
// Can't delete the last list — flash the border instead
selDelete.style.borderColor = '#c96b6b';
setTimeout(() => selDelete.style.borderColor = '', 800);
return;
}
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);
state.activeListId = state.lists[0].id;
saveState();
renderUpper();
overlay.remove();
});
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
}
// ─── Control Buttons ──────────────────────────────────────────────────────
btnEdit.addEventListener('click', () => {
if (!editMode) {
editMode = true;
btnEdit.textContent = 'Add';
btnReset.textContent = 'Save';
renderUpper();
} else {
showAddModal();
}
});
btnReset.addEventListener('click', () => {
if (editMode) {
editMode = false;
btnEdit.textContent = 'Edit';
btnReset.textContent = 'Reset';
renderUpper();
} else {
activeList().buttons.forEach(b => b.done = false);
saveState();
renderUpper();
}
});
selDropdown.addEventListener('change', () => {
const val = selDropdown.value;
if (val === '__new__') {
showNewListModal();
} else {
state.activeListId = val;
saveState();
renderUpper();
}
});
selDelete.addEventListener('click', () => showDeleteListConfirm());
selRename.addEventListener('click', () => showRenameListModal());
btnMinimize.addEventListener('click', () => {
state.minimized = !state.minimized;
saveState();
if (state.minimized) {
upper.style.display = 'none';
document.querySelector('#we-lower').style.display = 'none';
document.querySelector('#we-selector').style.display = 'none';
panel.style.height = 'auto';
btnMinimize.textContent = '▢';
} else {
upper.style.display = '';
document.querySelector('#we-lower').style.display = '';
document.querySelector('#we-selector').style.display = '';
panel.style.height = state.size.h + 'px';
btnMinimize.textContent = '—';
}
panel.classList.toggle('minimized', state.minimized);
resizeHandle.style.visibility = (state.minimized || state.locked) ? 'hidden' : 'visible';
});
btnLock.addEventListener('click', () => {
state.locked = !state.locked;
saveState();
btnLock.textContent = state.locked ? '🔒' : '🔓';
titlebar.style.cursor = state.locked ? 'default' : 'grab';
resizeHandle.style.visibility = (state.locked || state.minimized) ? 'hidden' : 'visible';
});
// Initial Setup
btnLock.textContent = state.locked ? '🔒' : '🔓';
titlebar.style.cursor = state.locked ? 'default' : 'grab';
resizeHandle.style.visibility = (state.locked || state.minimized) ? 'hidden' : 'visible';
if (state.minimized) {
upper.style.display = 'none';
document.querySelector('#we-lower').style.display = 'none';
document.querySelector('#we-selector').style.display = 'none';
panel.style.height = 'auto';
btnMinimize.textContent = '▢';
}
// ─── Drag ─────────────────────────────────────────────────────────────────
let dragging = false, dragStartX, dragStartY, panelStartX, panelStartY;
titlebar.addEventListener('mousedown', e => {
if (e.button !== 0 || state.locked || editMode) return;
dragging = true;
dragStartX = e.clientX;
dragStartY = e.clientY;
panelStartX = panel.offsetLeft;
panelStartY = panel.offsetTop;
e.preventDefault();
});
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';
});
document.addEventListener('mouseup', () => {
if (dragging) {
dragging = false;
state.pos = { x: panel.offsetLeft, y: panel.offsetTop };
saveState();
}
});
// ─── Resize ───────────────────────────────────────────────────────────────
let resizing = false, resStartX, resStartY, resStartW, resStartH;
resizeHandle.addEventListener('mousedown', e => {
if (e.button !== 0 || state.locked || state.minimized) 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 || state.minimized) return;
const t = e.touches[0];
resizing = true;
resStartX = t.clientX;
resStartY = t.clientY;
resStartW = panel.offsetWidth;
resStartH = panel.offsetHeight;
e.preventDefault();
e.stopPropagation();
}, { 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';
});
document.addEventListener('touchmove', e => {
if (!resizing) return;
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 (resizing) {
resizing = false;
state.size = { w: panel.offsetWidth, h: panel.offsetHeight };
saveState();
}
});
document.addEventListener('touchend', () => {
if (resizing) {
resizing = false;
state.size = { w: panel.offsetWidth, h: panel.offsetHeight };
saveState();
}
});
// ─── Init ─────────────────────────────────────────────────────────────────
function initDoneState() {
renderUpper();
}
window.addEventListener('resize', () => {
const rect = panel.getBoundingClientRect();
const newLeft = Math.min(panel.offsetLeft, window.innerWidth - rect.width);
const newTop = Math.min(panel.offsetTop, window.innerHeight - rect.height);
panel.style.left = Math.max(0, newLeft) + 'px';
panel.style.top = Math.max(0, newTop) + 'px';
state.pos = { x: panel.offsetLeft, y: panel.offsetTop };
saveState();
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDoneState);
} else if (document.body) {
initDoneState();
} else {
window.addEventListener('load', initDoneState);
}
})();