// ==UserScript==
// @name Milky Way Idle Tasklist
// @namespace http://tampermonkey.net/
// @version 2.0
// @description This script provides a persistent, draggable task list window for Milky Way Idle to help track your goals.
// @author Kjay
// @license MIT License
// @match https://www.milkywayidle.com/*
// @match https://test.milkywayidle.com/*
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_setValue
// ==/UserScript==
(function() {
'use strict';
// --- Configuration Settings ---
const checklistId = 'mwi-checklist';
const checklistTitle = 'Task List';
const minimizedLabelText = 'List:';
const MAX_ITEM_LENGTH = 40;
const TOOLTIP_LENGTH_THRESHOLD = 15;
const MAX_TOTAL_ITEMS = 15;
const STORAGE_KEY_POS = 'mwi_checklist_pos';
const STORAGE_KEY_ITEMS = 'mwi_checklist_items_v4';
const STORAGE_KEY_CONFIG = 'mwi_checklist_config_v5';
const STORAGE_KEY_LAST_SECTION = 'mwi_checklist_last_section';
const DEFAULT_POS = { top: '10px', left: null, right: '10px' };
const GLOBAL_SECTION_ID = "__global__";
const DEFAULT_CONFIG = { sections: [] };
// --- Styling ---
// Defines the CSS styles for the checklist UI elements and states.
const checklistStyle = `
#${checklistId} { position: fixed; background-color: #333; color: #eee; border: 1px solid #555; padding: 8px; width: 280px; z-index: 2000; display: flex; flex-direction: column; border-radius: 5px; font-family: sans-serif; overflow: hidden; cursor: default; max-height: 90vh; box-sizing: border-box; }
#${checklistId}.minimized { width: auto; height: auto; padding: 4px 8px; cursor: move; background-color: #222; border: 1px solid #444; border-radius: 5px; display: inline-flex; align-items: center; overflow: visible; max-height: none; }
#${checklistId}.minimized > *:not(.minimized-content-container) { display: none; }
#${checklistId} .minimized-content-container { display: none; align-items: center; }
#${checklistId}.minimized .minimized-content-container { display: inline-flex; }
#${checklistId} .minimized-label { color: #eee; margin-right: 5px; cursor: move; user-select: none; }
#${checklistId} .toggle-button { background-color: #4CAF50; color: white; padding: 2px 5px; border-radius: 3px; text-decoration: none; font-size: 0.9em; cursor: pointer; border: none; line-height: normal; vertical-align: middle; }
#${checklistId} .show-button { display: none; margin-left: 5px; }
#${checklistId}.minimized .show-button { display: inline-block; }
#${checklistId} .header-buttons-container { position: absolute; top: 6px; right: 6px; display: flex; gap: 4px; align-items: center; z-index: 1; }
#${checklistId} .hide-button { order: 1; }
#${checklistId} h3 { margin: -8px -8px 5px -8px; padding: 5px 35px 5px 8px; text-align: center; cursor: move; background-color: #444; color: #fff; font-size: 1.2em; flex: 0 0 auto; position: relative; border-bottom: 1px solid #555; user-select: none; }
#${checklistId} #settingsToggleLabel { font-size: 0.9em; color: #ccc; cursor: pointer; margin: 0 0 5px 0; display: block; text-align: center; flex-shrink: 0; border-top: 1px solid #555; padding-top: 5px; }
#${checklistId}.minimized #settingsToggleLabel { display: none; }
#${checklistId} #settingsArea { display: none; flex-direction: column; gap: 8px; background-color: #3a3a3a; padding: 8px; margin-bottom: 5px; border-radius: 3px; border: 1px solid #555; font-size: 0.9em; flex-shrink: 0; }
#${checklistId}.minimized #settingsArea { display: none !important; }
#${checklistId} #settingsArea > div:not(.settings-section-name) { display: flex; align-items: center; margin-bottom: 5px; }
#${checklistId} #settingsArea label { margin-right: 5px; display: inline-block; width: 60px; text-align: right; flex-shrink: 0; }
#${checklistId} #settingsArea input[type=text] { background-color: #444; color: #eee; border: 1px solid #555; padding: 2px 4px; border-radius: 3px; margin-right: 5px; flex-grow: 1; }
#${checklistId} #settingsArea select { background-color: #444; color: #eee; border: 1px solid #555; padding: 2px; border-radius: 3px; }
#${checklistId} #settingsArea button { font-size: 0.9em; padding: 2px 5px; background-color: #4CAF50; border: none; color: white; cursor: pointer; }
#${checklistId} .settings-section-name { display: none; flex-direction: column; margin-bottom: 8px; padding-left: 5px; border-left: 2px solid transparent; }
#${checklistId} .settings-section-name.visible { display: flex; }
#${checklistId} .settings-input-row { display: flex; align-items: center; width: 100%; }
#${checklistId} .remove-indicator { font-size: 0.85em; color: #ffc107; margin-left: 68px; margin-top: 2px; font-style: italic; display: none; line-height: 1.2; }
#${checklistId} .settings-section-name.marked-for-removal { border-left-color: #ffc107; padding-left: 3px; }
#${checklistId} .settings-section-name.marked-for-removal .remove-indicator { display: block; }
#${checklistId} .list-controls { display: flex; justify-content: center; align-items: center; padding: 0 5px 5px 5px; font-size: 0.85em; color: #bbb; flex-shrink: 0; position: relative; height: auto; margin-bottom: 5px; }
#${checklistId}.minimized .list-controls { display: none; }
#${checklistId} #taskCounter { flex-grow: 1; text-align: center; line-height: 18px; }
#${checklistId} #taskCounter.full { color: #ffc107; font-weight: bold; }
#${checklistId} #addButton { background-color: #4CAF50; border: none; color: white; padding: 3px 6px; text-align: center; text-decoration: none; display: inline-block; font-size: 0.9em; cursor: pointer; border-radius: 3px; flex-shrink: 0; }
#${checklistId} .clear-list-button { background-color: #fd7e14 !important; color: white; border: none; padding: 1px 4px; font-size: 0.8em; border-radius: 3px; cursor: pointer; }
#${checklistId} #clearAllButton { position: absolute; right: 5px; top: 0px; }
#${checklistId} .delete-button { background-color: #f44336; padding: 3px 4px; flex-shrink: 0; border: none; color: white; font-size: 0.9em; cursor: pointer; border-radius: 3px; }
#${checklistId} #sectionContainer { flex-grow: 1; overflow-y: auto; min-height: 50px; border-top: 1px solid #555; padding-top: 5px; margin-top: 0; padding-right: 4px; }
#${checklistId} .task-section { margin-bottom: 10px; }
#${checklistId} .task-section h4 { margin: 0 0 0 3px; font-size: 0.95em; color: #ddd; border-bottom: none; padding-bottom: 0; flex-grow: 1; }
#${checklistId} .section-header { display: flex; justify-content: space-between; align-items: center; margin: 0 0 3px 0; border-bottom: 1px dotted #666; padding-bottom: 2px; padding-right: 4px; cursor: default; }
#${checklistId} .clear-section-button { background-color: #fd7e14; color: white; border: none; padding: 0px 3px; font-size: 0.75em; border-radius: 3px; cursor: pointer; line-height: 1.4; flex-shrink: 0; }
#${checklistId} .task-section ul, #${checklistId} .completed-section ul { list-style: none; padding: 5px 0; margin: 0; min-height: 15px; position: relative; box-sizing: border-box; }
#${checklistId} ul { list-style: none; padding: 0; margin: 0; }
#${checklistId} li { margin-bottom: 3px; display: flex; align-items: center; padding: 2px 0; border: 1px solid transparent; transition: background-color 0.1s ease; position: relative; }
#${checklistId} li.dragging { outline: 2px dashed #aaa; background: #444; opacity: 0.7; }
#${checklistId} li.drag-over-before { border-top: 2px solid #4CAF50; }
#${checklistId} li.drag-over-after { border-bottom: 2px solid #4CAF50; }
#${checklistId} ul.drag-over-empty { border-top: 2px solid #4CAF50; margin-top: -2px; }
#${checklistId} .drag-handle { cursor: grab; margin-right: 8px; margin-left: 3px; opacity: 0.7; user-select: none; line-height: 1; padding: 0 3px; }
#${checklistId} .drag-handle:hover { opacity: 1; }
#${checklistId} input[type="checkbox"] { margin-right: 5px; cursor: pointer; flex-shrink: 0; }
#${checklistId} .item-text { flex-grow: 1; margin-right: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: text; }
#${checklistId} .edit-input { flex-grow: 1; margin-right: 5px; background-color: #555; color: #eee; border: 1px solid #777; padding: 1px 3px; border-radius: 2px; font-size: inherit; font-family: inherit; }
#${checklistId} #addItemDiv { display: flex; flex-direction: column; flex-shrink: 0; margin-top: 8px; border-top: 1px solid #555; padding-top: 8px; }
#${checklistId}.minimized #addItemDiv { display: none; }
#${checklistId} .add-item-row1 { display: flex; align-items: center; margin-bottom: 4px; }
#${checklistId} #addSectionLabel { margin-right: 5px; font-size: 0.9em; color: #ccc; flex-shrink: 0; }
#${checklistId} #sectionSelect { background-color: #444; color: #eee; border: 1px solid #555; padding: 3px; border-radius: 3px; font-size: 0.9em; flex-grow: 1; max-width: none; }
#${checklistId} .add-item-row2 { display: flex; align-items: center; }
#${checklistId} #addItemInput { flex-grow: 1; margin-right: 5px; background-color: #444; color: #eee; border: 1px solid #555; padding: 3px 5px; border-radius: 3px; min-width: 50px; }
#${checklistId} #addItemInput:disabled, #${checklistId} #addButton:disabled, #${checklistId} #sectionSelect:disabled { opacity: 0.5; cursor: not-allowed; }
#${checklistId} .completed-section { margin-top: 8px; border-top: 1px solid #555; padding-top: 5px; flex-shrink: 0; }
#${checklistId}.minimized .completed-section { display: none; }
#${checklistId} .completed-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 3px; cursor: default; /* Add default cursor */ }
#${checklistId} .completed-section ul { opacity: 0.7; max-height: 150px; overflow-y: auto; }
#${checklistId} .completed-section h4 { margin: 0; font-size: 0.9em; color: #bbb; }
#${checklistId} [title] { cursor: help; }
#${checklistId} .task-section.drag-over-section, #${checklistId} .completed-section.drag-over-section { background-color: #404040 !important; outline: 1px dashed #888; outline-offset: -1px; }
`;
GM_addStyle(checklistStyle);
// --- Helper: Generate Unique ID ---
function generateUniqueId() {
return Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
}
// --- Storage & Config Functions ---
function getSavedConfig() {
let configJson = GM_getValue(STORAGE_KEY_CONFIG, JSON.stringify(DEFAULT_CONFIG));
let config;
try {
config = JSON.parse(configJson);
} catch (e) {
console.error("MWI Tasklist: Error parsing config JSON. Resetting.", e, configJson);
config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
}
let needsSave = false;
if (config && config.hasOwnProperty('count') && config.hasOwnProperty('names') && !config.hasOwnProperty('sections')) {
console.log("MWI Tasklist: Migrating config from old format.");
const newConfig = { sections: [] };
for (let i = 0; i < config.count; i++) {
const name = config.names[i] || `Character ${i + 1}`;
newConfig.sections.push({ id: generateUniqueId(), name: name });
}
config = newConfig;
needsSave = true;
}
if (!config || typeof config !== 'object' || !Array.isArray(config.sections)) {
console.warn("MWI Tasklist: Invalid config structure found. Resetting to default.");
config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
needsSave = true;
}
config.sections = config.sections.map(s => {
if (!s || typeof s !== 'object') return null;
if (!s.id) {
s.id = generateUniqueId();
needsSave = true;
}
s.name = String(s.name || '').trim().substring(0, MAX_ITEM_LENGTH);
if (!s.name) {
s.name = "Unnamed Section";
needsSave = true;
}
return s;
}).filter(s => s !== null);
if (needsSave) {
console.log("MWI Tasklist: Saving updated/migrated config.");
saveConfig(config);
}
return config;
}
function saveConfig(config) {
if (!config || !Array.isArray(config.sections)) {
console.error("MWI Tasklist: Attempted to save invalid config structure.", config);
return;
}
GM_setValue(STORAGE_KEY_CONFIG, JSON.stringify(config));
}
function getChecklistItems() {
let itemsJson = GM_getValue(STORAGE_KEY_ITEMS, '[]');
let items;
try {
items = JSON.parse(itemsJson);
} catch (e) {
console.error("MWI Tasklist: Error parsing items JSON. Resetting.", e, itemsJson);
items = [];
}
if (!Array.isArray(items)) {
console.warn("MWI Tasklist: Invalid items structure found. Resetting to empty array.");
items = [];
}
let needsSave = false;
items = items.map(item => {
if (!item || typeof item !== 'object') return null;
if (!item.id) {
item.id = generateUniqueId();
needsSave = true;
}
if (item.sectionId === undefined || item.sectionId === null) {
item.sectionId = GLOBAL_SECTION_ID;
needsSave = true;
}
item.text = String(item.text || '').trim().substring(0, MAX_ITEM_LENGTH);
item.checked = Boolean(item.checked);
return item;
}).filter(item => item !== null);
if (items.length > MAX_TOTAL_ITEMS) {
console.warn(`MWI Tasklist: Exceeded MAX_TOTAL_ITEMS (${MAX_TOTAL_ITEMS}). Truncating list.`);
items = items.slice(0, MAX_TOTAL_ITEMS);
needsSave = true;
}
if (needsSave) {
console.log("MWI Tasklist: Saving updated/validated items.");
GM_setValue(STORAGE_KEY_ITEMS, JSON.stringify(items));
}
return items;
}
function setChecklistItems(items) {
if (!Array.isArray(items)) {
console.error("MWI Tasklist: Attempted to save invalid items array.", items);
return;
}
if (items.length > MAX_TOTAL_ITEMS) {
items = items.slice(0, MAX_TOTAL_ITEMS);
}
GM_setValue(STORAGE_KEY_ITEMS, JSON.stringify(items));
}
function findActualItemIndexById(itemId, currentItems) {
if (!itemId || !Array.isArray(currentItems)) return -1;
return currentItems.findIndex(i => i && i.id === itemId);
}
// --- Position Handling ---
function savePosition(element) {
if (!element) return;
if (element.style.left) {
const pos = {
top: element.style.top,
left: element.style.left,
};
GM_setValue(STORAGE_KEY_POS, JSON.stringify(pos));
} else {
const pos = {
top: element.style.top,
right: element.style.right || DEFAULT_POS.right,
};
GM_setValue(STORAGE_KEY_POS, JSON.stringify(pos));
}
}
function loadPosition() {
const savedPos = GM_getValue(STORAGE_KEY_POS, null);
if (savedPos) {
try {
const pos = JSON.parse(savedPos);
if (pos && typeof pos.top === 'string') {
if (typeof pos.left === 'string') {
return { top: pos.top, left: pos.left, right: null };
} else if (typeof pos.right === 'string') {
return { top: pos.top, left: null, right: pos.right };
}
}
} catch (e) {
console.error("MWI Tasklist: Error parsing saved position:", e);
GM_setValue(STORAGE_KEY_POS, null);
}
}
return { ...DEFAULT_POS };
}
function applyPosition(element, pos) {
if (!element || !pos) return;
element.style.top = pos.top || '';
element.style.left = pos.left || '';
element.style.right = pos.right || '';
if (pos.left) element.style.right = '';
if (pos.right) element.style.left = '';
}
// --- UI Elements Creation Functions ---
function createChecklistItemElement(item) {
const listItem = document.createElement('li');
listItem.dataset.itemId = item.id;
listItem.dataset.itemSection = item.sectionId;
const dragHandle = document.createElement('span');
dragHandle.innerHTML = '☰';
dragHandle.className = 'drag-handle';
dragHandle.title = 'Drag to reorder';
dragHandle.draggable = !item.checked;
listItem.appendChild(dragHandle);
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.checked = item.checked;
checkbox.title = item.checked ? 'Mark as incomplete' : 'Mark as complete';
checkbox.addEventListener('change', () => {
const items = getChecklistItems();
const actualIndex = findActualItemIndexById(item.id, items);
if (actualIndex !== -1) {
items[actualIndex].checked = checkbox.checked;
setChecklistItems(items);
renderChecklist();
} else {
console.warn("MWI Tasklist: Could not find item by ID to update checkbox state:", item.id);
}
});
const itemTextSpan = document.createElement('span');
itemTextSpan.className = 'item-text';
itemTextSpan.textContent = item.text || '';
if (item.text && item.text.length > TOOLTIP_LENGTH_THRESHOLD) {
itemTextSpan.title = item.text;
}
itemTextSpan.addEventListener('dblclick', () => {
if (item.checked) return;
startEditing(listItem, itemTextSpan, item.id);
});
const deleteButton = document.createElement('button');
deleteButton.textContent = 'X';
deleteButton.className = 'delete-button';
deleteButton.title = 'Delete task';
deleteButton.addEventListener('click', () => {
const items = getChecklistItems();
const actualIndex = findActualItemIndexById(item.id, items);
if(actualIndex !== -1) {
items.splice(actualIndex, 1);
setChecklistItems(items);
renderChecklist();
} else {
console.warn("MWI Tasklist: Could not find item by ID to delete:", item.id);
}
});
listItem.appendChild(checkbox);
listItem.appendChild(itemTextSpan);
listItem.appendChild(deleteButton);
if (item.checked) {
listItem.style.opacity = '0.6';
itemTextSpan.style.textDecoration = 'line-through';
dragHandle.style.cursor = 'default';
dragHandle.style.opacity = '0.2';
itemTextSpan.style.cursor = 'default';
}
return listItem;
}
// --- Edit Functionality ---
function startEditing(listItem, textSpan, itemId) {
if (listItem.querySelector('.edit-input')) return;
const currentText = textSpan.textContent;
const editInput = document.createElement('input');
editInput.type = 'text';
editInput.className = 'edit-input';
editInput.value = currentText;
editInput.maxLength = MAX_ITEM_LENGTH;
listItem.replaceChild(editInput, textSpan);
editInput.focus();
editInput.select();
const cleanup = () => {
editInput.removeEventListener('blur', saveEdit);
editInput.removeEventListener('keydown', handleKeydown);
if (listItem.contains(editInput)) {
listItem.replaceChild(textSpan, editInput);
}
};
const saveEdit = () => {
let newText = editInput.value.trim();
listItem.replaceChild(textSpan, editInput);
if (newText && newText !== currentText) {
const items = getChecklistItems();
const actualIndex = findActualItemIndexById(itemId, items);
if (actualIndex !== -1) {
items[actualIndex].text = newText;
setChecklistItems(items);
textSpan.textContent = newText;
if (newText.length > TOOLTIP_LENGTH_THRESHOLD) {
textSpan.title = newText;
} else {
textSpan.removeAttribute('title');
}
} else {
console.warn("MWI Tasklist: Could not find item by ID to save edit:", itemId);
textSpan.textContent = currentText;
}
} else if (!newText) {
textSpan.textContent = currentText;
console.log("MWI Tasklist: Edit cancelled, new text is empty.");
} else {
textSpan.textContent = currentText;
}
cleanup();
};
const cancelEdit = () => {
listItem.replaceChild(textSpan, editInput);
textSpan.textContent = currentText;
cleanup();
};
const handleKeydown = (e) => {
if (e.key === 'Enter') {
e.preventDefault();
saveEdit();
} else if (e.key === 'Escape') {
cancelEdit();
}
};
editInput.addEventListener('blur', saveEdit);
editInput.addEventListener('keydown', handleKeydown);
}
// --- Render Checklist ---
function renderChecklist() {
const checklistDiv = document.getElementById(checklistId);
if (!checklistDiv || checklistDiv.classList.contains('minimized')) {
return;
}
const elements = {
sectionContainer: checklistDiv.querySelector('#sectionContainer'),
completedUl: checklistDiv.querySelector('.completed-section ul'),
taskCounter: checklistDiv.querySelector('#taskCounter'),
addItemInput: checklistDiv.querySelector('#addItemInput'),
addButton: checklistDiv.querySelector('#addButton'),
sectionSelect: checklistDiv.querySelector('#sectionSelect')
};
if (!elements.sectionContainer || !elements.completedUl || !elements.taskCounter || !elements.addItemInput || !elements.addButton || !elements.sectionSelect) {
console.error("MWI Tasklist: Checklist UI elements missing during render!");
return;
}
const config = getSavedConfig();
const items = getChecklistItems();
const totalItemCount = items.length;
let activeItemCount = 0;
const lastSectionId = GM_getValue(STORAGE_KEY_LAST_SECTION, GLOBAL_SECTION_ID);
const scrollPositions = new Map();
elements.sectionContainer.querySelectorAll('ul[data-section-id]').forEach(ul => {
scrollPositions.set(ul.dataset.sectionId, ul.scrollTop);
});
if (elements.completedUl) {
scrollPositions.set('__completed__', elements.completedUl.scrollTop);
}
elements.sectionContainer.innerHTML = '';
elements.completedUl.innerHTML = '';
elements.sectionSelect.innerHTML = '';
const sectionMap = new Map();
const createOption = (value, text, isSelected) => {
const option = document.createElement('option');
option.value = value;
option.textContent = text;
if (isSelected) option.selected = true;
elements.sectionSelect.appendChild(option);
};
createOption(GLOBAL_SECTION_ID, 'Global', lastSectionId === GLOBAL_SECTION_ID);
const createSectionDOM = (titleText, sectionId) => {
const sectionDiv = document.createElement('div');
sectionDiv.className = 'task-section';
const headerDiv = document.createElement('div');
headerDiv.className = 'section-header';
const title = document.createElement('h4');
title.textContent = titleText;
headerDiv.appendChild(title);
const clearButton = document.createElement('button');
clearButton.textContent = 'Clear';
clearButton.className = 'clear-section-button clear-list-button';
clearButton.title = `Clear active tasks in '${titleText}'`;
clearButton.dataset.sectionId = sectionId;
clearButton.addEventListener('click', handleClearSection);
headerDiv.appendChild(clearButton);
sectionDiv.appendChild(headerDiv);
const ul = document.createElement('ul');
ul.dataset.sectionId = sectionId;
sectionDiv.appendChild(ul);
elements.sectionContainer.appendChild(sectionDiv);
sectionMap.set(sectionId, ul);
// Attach drag listeners to the list (UL) for item handling
enableDragSort(ul);
// Attach drag listeners to the header DIV for dropping onto the section header
enableDragSort(headerDiv);
};
createSectionDOM('Global Tasks', GLOBAL_SECTION_ID);
config.sections.forEach(section => {
if (!section || !section.id || !section.name) return;
createOption(section.id, section.name, lastSectionId === section.id);
createSectionDOM(section.name, section.id);
});
items.forEach((item) => {
if (!item || !item.id) return;
const listItem = createChecklistItemElement(item);
if (item.checked) {
elements.completedUl.appendChild(listItem);
} else {
activeItemCount++;
const currentItemSectionId = item.sectionId || GLOBAL_SECTION_ID;
const targetUl = sectionMap.get(currentItemSectionId);
if (targetUl) {
targetUl.appendChild(listItem);
} else {
console.warn(`MWI Tasklist: Item "${item.text}" belongs to non-existent section "${currentItemSectionId}". Moving to Global.`);
sectionMap.get(GLOBAL_SECTION_ID).appendChild(listItem);
}
}
});
let counterText = `(${activeItemCount} / ${MAX_TOTAL_ITEMS})`;
elements.taskCounter.classList.remove('full');
if (totalItemCount >= MAX_TOTAL_ITEMS) {
counterText += ' *FULL*';
elements.taskCounter.classList.add('full');
}
elements.taskCounter.textContent = counterText;
const isListFull = totalItemCount >= MAX_TOTAL_ITEMS;
elements.addItemInput.disabled = isListFull;
elements.addButton.disabled = isListFull;
sectionMap.forEach((ul, sectionId) => {
if (scrollPositions.has(sectionId)) {
ul.scrollTop = scrollPositions.get(sectionId);
}
});
if (elements.completedUl && scrollPositions.has('__completed__')) {
elements.completedUl.scrollTop = scrollPositions.get('__completed__');
}
enableDragSort(elements.completedUl);
const completedHeader = checklistDiv.querySelector('.completed-section .completed-header');
if(completedHeader) enableDragSort(completedHeader);
}
// --- Event Handler for Clearing a Section ---
function handleClearSection(event) {
const sectionIdToClear = event.target.dataset.sectionId;
if (!sectionIdToClear) return;
const config = getSavedConfig();
const sectionInfo = config.sections.find(s => s.id === sectionIdToClear);
const sectionName = sectionIdToClear === GLOBAL_SECTION_ID ? "Global Tasks" : (sectionInfo?.name || `Section ${sectionIdToClear}`);
if (confirm(`Clear all ACTIVE tasks in the "${sectionName}" section? (Completed tasks will remain)`)) {
const currentItems = getChecklistItems();
const itemsToKeep = currentItems.filter(item => {
return item.checked || (!item.checked && (item.sectionId || GLOBAL_SECTION_ID) !== sectionIdToClear);
});
setChecklistItems(itemsToKeep);
renderChecklist();
}
}
// --- Settings Handlers ---
function toggleSettingsArea() {
const settingsArea = document.getElementById('settingsArea');
const settingsLabel = document.getElementById('settingsToggleLabel');
if (!settingsArea || !settingsLabel) return;
const isVisible = settingsArea.style.display === 'flex';
settingsArea.style.display = isVisible ? 'none' : 'flex';
settingsLabel.textContent = isVisible ? 'Task Sections Settings ▼' : 'Task Sections Settings ▲';
if (!isVisible) {
const currentConfig = getSavedConfig();
const countSelect = document.getElementById('settingsSectionCount');
countSelect.value = currentConfig.sections.length;
for(let i = 0; i < 3; i++) {
const nameInput = settingsArea.querySelector(`#settingsSectionName${i+1}`);
const inputDiv = settingsArea.querySelector(`#settingsSectionNameDiv${i+1}`);
if (nameInput && inputDiv) {
const sectionData = currentConfig.sections[i];
nameInput.value = sectionData ? sectionData.name : '';
inputDiv.classList.toggle('visible', i < currentConfig.sections.length);
inputDiv.classList.remove('marked-for-removal');
}
}
updateSectionNameInputs(currentConfig.sections.length, currentConfig.sections.length);
}
}
function updateSectionNameInputs(selectedCount, savedCount) {
const settingsArea = document.getElementById('settingsArea');
if (!settingsArea) return;
for (let i = 0; i < 3; i++) {
const inputDiv = settingsArea.querySelector(`#settingsSectionNameDiv${i+1}`);
const nameInput = inputDiv?.querySelector(`#settingsSectionName${i+1}`);
if (inputDiv && nameInput) {
const shouldBeVisibleForSelection = i < selectedCount;
const existsInSavedConfig = i < savedCount;
const shouldDisplay = shouldBeVisibleForSelection || (existsInSavedConfig && !shouldBeVisibleForSelection);
inputDiv.classList.toggle('visible', shouldDisplay);
const markedForRemoval = existsInSavedConfig && !shouldBeVisibleForSelection;
inputDiv.classList.toggle('marked-for-removal', markedForRemoval);
}
}
}
function saveSettingsHandler() {
const currentConfig = getSavedConfig();
const countSelect = document.getElementById('settingsSectionCount');
const newCount = parseInt(countSelect.value, 10);
const newSections = [];
const preservedSectionIds = new Set([GLOBAL_SECTION_ID]);
let itemsModified = false;
const namesEncountered = new Set();
for (let i = 0; i < newCount; i++) {
const nameInput = document.getElementById(`settingsSectionName${i+1}`);
let baseName = (nameInput.value.trim() || `Character ${i+1}`).substring(0, MAX_ITEM_LENGTH);
let name = baseName;
let duplicateCounter = 1;
while (namesEncountered.has(name) || name === 'Global') {
name = `${baseName}_${duplicateCounter++}`.substring(0, MAX_ITEM_LENGTH);
}
namesEncountered.add(name);
const existingSection = (i < currentConfig.sections.length) ? currentConfig.sections[i] : null;
const id = existingSection ? existingSection.id : generateUniqueId();
newSections.push({ id: id, name: name });
preservedSectionIds.add(id);
}
const currentItems = getChecklistItems();
const updatedItems = currentItems.map(item => {
if (!item.checked && item.sectionId && item.sectionId !== GLOBAL_SECTION_ID && !preservedSectionIds.has(item.sectionId)) {
console.log(`MWI Tasklist: Moving item "${item.text}" from removed section ${item.sectionId} to Global.`);
item.sectionId = GLOBAL_SECTION_ID;
itemsModified = true;
}
return item;
});
if (itemsModified) {
setChecklistItems(updatedItems);
}
saveConfig({ sections: newSections });
toggleSettingsArea();
renderChecklist();
}
// --- Create UI ---
function createChecklistUI() {
if (document.getElementById(checklistId)) return;
const checklistDiv = document.createElement('div');
checklistDiv.id = checklistId;
let offsetX, offsetY;
let isWindowDragging = false;
function startDrag(e) {
if (e.target.closest('button, input, select, a, .drag-handle')) return;
if (!checklistDiv.classList.contains('minimized') && !e.target.matches('h3')) return;
if (checklistDiv.classList.contains('minimized') && !e.target.matches('.minimized-label, #' + checklistId + '.minimized')) return;
isWindowDragging = true;
checklistDiv.style.cursor = 'grabbing';
const rect = checklistDiv.getBoundingClientRect();
offsetX = e.clientX - rect.left;
offsetY = e.clientY - rect.top;
checklistDiv.style.right = '';
checklistDiv.style.left = rect.left + 'px';
checklistDiv.style.top = rect.top + 'px';
document.addEventListener('mousemove', dragWindow);
document.addEventListener('mouseup', stopWindowDrag, { once: true });
e.preventDefault();
}
function dragWindow(e) {
if (!isWindowDragging) return;
let newX = e.clientX - offsetX;
let newY = e.clientY - offsetY;
newX = Math.max(0, Math.min(newX, window.innerWidth - checklistDiv.offsetWidth));
newY = Math.max(0, Math.min(newY, window.innerHeight - checklistDiv.offsetHeight));
checklistDiv.style.left = newX + 'px';
checklistDiv.style.top = newY + 'px';
}
function stopWindowDrag() {
if (!isWindowDragging) return;
isWindowDragging = false;
checklistDiv.style.cursor = checklistDiv.classList.contains('minimized') ? 'move' : 'default';
savePosition(checklistDiv);
document.removeEventListener('mousemove', dragWindow);
}
function addItem() {
const addItemInput = document.getElementById('addItemInput');
const sectionSelect = document.getElementById('sectionSelect');
if (!addItemInput || !sectionSelect) return;
const selectedSectionId = sectionSelect.value;
let newItemText = addItemInput.value.trim();
if (newItemText.length > MAX_ITEM_LENGTH) {
newItemText = newItemText.substring(0, MAX_ITEM_LENGTH);
addItemInput.value = newItemText;
}
if (newItemText) {
const currentItems = getChecklistItems();
if (currentItems.length >= MAX_TOTAL_ITEMS) {
alert(`Maximum number of tasks (${MAX_TOTAL_ITEMS}) reached.`);
return;
}
const newItem = {
id: generateUniqueId(),
text: newItemText,
checked: false,
sectionId: selectedSectionId
};
currentItems.push(newItem);
setChecklistItems(currentItems);
GM_setValue(STORAGE_KEY_LAST_SECTION, selectedSectionId);
renderChecklist();
addItemInput.value = '';
addItemInput.focus();
}
}
applyPosition(checklistDiv, loadPosition());
checklistDiv.classList.add('minimized');
const minimizedContainer = document.createElement('div');
minimizedContainer.className = 'minimized-content-container';
const minimizedLabel = document.createElement('span');
minimizedLabel.className = 'minimized-label';
minimizedLabel.textContent = minimizedLabelText;
minimizedContainer.appendChild(minimizedLabel);
const showButton = document.createElement('a');
showButton.href = '#';
showButton.role = 'button';
showButton.className = 'show-button toggle-button';
showButton.textContent = 'Show';
showButton.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
checklistDiv.classList.remove('minimized');
applyPosition(checklistDiv, loadPosition());
renderChecklist();
});
minimizedContainer.appendChild(showButton);
checklistDiv.appendChild(minimizedContainer);
const title = document.createElement('h3');
title.textContent = checklistTitle;
title.addEventListener('mousedown', startDrag);
checklistDiv.appendChild(title);
const headerButtonsContainer = document.createElement('div');
headerButtonsContainer.className = 'header-buttons-container';
const hideButton = document.createElement('button');
hideButton.className = 'hide-button toggle-button';
hideButton.textContent = 'Hide';
hideButton.title = 'Minimize list';
hideButton.addEventListener('click', (e) => {
e.stopPropagation();
savePosition(checklistDiv);
checklistDiv.classList.add('minimized');
});
headerButtonsContainer.appendChild(hideButton);
checklistDiv.appendChild(headerButtonsContainer);
const listControlsDiv = document.createElement('div');
listControlsDiv.className = 'list-controls';
const taskCounterDiv = document.createElement('span');
taskCounterDiv.id = 'taskCounter';
taskCounterDiv.textContent = `(0 / ${MAX_TOTAL_ITEMS})`;
listControlsDiv.appendChild(taskCounterDiv);
const clearAllButton = document.createElement('button');
clearAllButton.id = 'clearAllButton';
clearAllButton.textContent = 'Clear All';
clearAllButton.title = 'Delete ALL tasks (active and completed)';
clearAllButton.classList.add('clear-list-button');
clearAllButton.addEventListener('click', () => {
if (confirm('Are you sure you want to delete ALL tasks? This cannot be undone.')) {
setChecklistItems([]);
renderChecklist();
}
});
listControlsDiv.appendChild(clearAllButton);
checklistDiv.appendChild(listControlsDiv);
const settingsToggleLabel = document.createElement('div');
settingsToggleLabel.id = 'settingsToggleLabel';
settingsToggleLabel.textContent = 'Task Sections Settings ▼';
settingsToggleLabel.title = 'Show/hide section management';
settingsToggleLabel.style.cursor = 'pointer';
settingsToggleLabel.addEventListener('click', toggleSettingsArea);
checklistDiv.appendChild(settingsToggleLabel);
const settingsArea = document.createElement('div');
settingsArea.id = 'settingsArea';
settingsArea.innerHTML = `
<div>
<label for="settingsSectionCount">Sections:</label>
<select id="settingsSectionCount" title="Number of custom sections (0-3)">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
</div>
<div id="settingsSectionNameDiv1" class="settings-section-name">
<div class="settings-input-row">
<label for="settingsSectionName1">Name 1:</label>
<input type="text" id="settingsSectionName1" maxlength="${MAX_ITEM_LENGTH}">
</div>
<span class="remove-indicator">(tasks will move to Global)</span>
</div>
<div id="settingsSectionNameDiv2" class="settings-section-name">
<div class="settings-input-row">
<label for="settingsSectionName2">Name 2:</label>
<input type="text" id="settingsSectionName2" maxlength="${MAX_ITEM_LENGTH}">
</div>
<span class="remove-indicator">(tasks will move to Global)</span>
</div>
<div id="settingsSectionNameDiv3" class="settings-section-name">
<div class="settings-input-row">
<label for="settingsSectionName3">Name 3:</label>
<input type="text" id="settingsSectionName3" maxlength="${MAX_ITEM_LENGTH}">
</div>
<span class="remove-indicator">(tasks will move to Global)</span>
</div>
<button id="saveSettingsButton" title="Save section changes">Save Settings</button>
`;
checklistDiv.appendChild(settingsArea);
const countSelect = settingsArea.querySelector('#settingsSectionCount');
countSelect.addEventListener('change', () => {
const currentConfig = getSavedConfig();
updateSectionNameInputs(parseInt(countSelect.value, 10), currentConfig.sections.length);
});
settingsArea.querySelector('#saveSettingsButton').addEventListener('click', saveSettingsHandler);
const sectionContainer = document.createElement('div');
sectionContainer.id = 'sectionContainer';
checklistDiv.appendChild(sectionContainer);
const completedSection = document.createElement('div');
completedSection.className = 'completed-section';
const completedHeaderDiv = document.createElement('div');
completedHeaderDiv.className = 'completed-header';
const completedTitle = document.createElement('h4');
completedTitle.textContent = 'Completed';
completedHeaderDiv.appendChild(completedTitle);
const clearCompletedButton = document.createElement('button');
clearCompletedButton.textContent = 'Clear';
clearCompletedButton.classList.add('clear-list-button');
clearCompletedButton.title = 'Delete all completed tasks';
clearCompletedButton.addEventListener('click', () => {
if (confirm('Are you sure you want to delete all COMPLETED tasks?')) {
const items = getChecklistItems();
const activeItems = items.filter(item => !item.checked);
setChecklistItems(activeItems);
renderChecklist();
}
});
completedHeaderDiv.appendChild(clearCompletedButton);
completedSection.appendChild(completedHeaderDiv);
const completedUl = document.createElement('ul');
completedUl.dataset.sectionId = "__completed__";
completedSection.appendChild(completedUl);
checklistDiv.appendChild(completedSection);
const addItemDiv = document.createElement('div');
addItemDiv.id = "addItemDiv";
const addItemRow1 = document.createElement('div');
addItemRow1.className = 'add-item-row1';
const addSectionLabel = document.createElement('label');
addSectionLabel.id = 'addSectionLabel';
addSectionLabel.textContent = 'Add To:';
addSectionLabel.htmlFor = 'sectionSelect';
const sectionSelectDropdown = document.createElement('select');
sectionSelectDropdown.id = 'sectionSelect';
sectionSelectDropdown.title = 'Choose section for new task';
addItemRow1.appendChild(addSectionLabel);
addItemRow1.appendChild(sectionSelectDropdown);
const addItemRow2 = document.createElement('div');
addItemRow2.className = 'add-item-row2';
const addItemInput = document.createElement('input');
addItemInput.id = 'addItemInput';
addItemInput.type = 'text';
addItemInput.placeholder = 'New item text';
addItemInput.maxLength = MAX_ITEM_LENGTH;
addItemInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter' && !addItemInput.disabled) {
event.preventDefault();
addItem();
}
});
const addButton = document.createElement('button');
addButton.id = 'addButton';
addButton.textContent = 'Add';
addButton.title = 'Add new task to selected section';
addButton.addEventListener('click', addItem);
addItemRow2.appendChild(addItemInput);
addItemRow2.appendChild(addButton);
addItemDiv.appendChild(addItemRow1);
addItemDiv.appendChild(addItemRow2);
checklistDiv.appendChild(addItemDiv);
checklistDiv.addEventListener('mousedown', startDrag);
document.body.appendChild(checklistDiv);
}
// --- Drag and Drop Sorting for List Items ---
let draggedItemElement = null;
// Function to attach drag event listeners to an element (UL or Header)
function enableDragSort(element) {
// Use capturing phase for dragstart to ensure it's handled correctly before potential bubbling issues
element.removeEventListener('dragstart', handleDragStart, true);
element.addEventListener('dragstart', handleDragStart, true);
element.removeEventListener('dragend', handleDragEnd);
element.addEventListener('dragend', handleDragEnd);
element.removeEventListener('dragover', handleDragOver);
element.addEventListener('dragover', handleDragOver);
element.removeEventListener('dragleave', handleDragLeave);
element.addEventListener('dragleave', handleDragLeave);
element.removeEventListener('drop', handleDrop);
element.addEventListener('drop', handleDrop);
}
// Called when dragging starts (attached to UL and Header elements in capturing phase)
function handleDragStart(e) {
// Check if the direct target of the event is the drag handle
if (e.target.classList.contains('drag-handle')) {
// This is the intended drag start, proceed
draggedItemElement = e.target.closest('li');
if (!draggedItemElement) return; // Should have a parent li
setTimeout(() => {
if (draggedItemElement) draggedItemElement.classList.add('dragging');
}, 0);
e.dataTransfer.effectAllowed = 'move';
try {
e.dataTransfer.setData('text/plain', draggedItemElement.dataset.itemId);
} catch (err) {
console.warn("MWI Tasklist: Could not set drag data.", err);
}
} else {
if (e.target.closest(`#${checklistId}`)) {
e.preventDefault();
}
}
}
// Called when dragging ends
function handleDragEnd(e) {
// Cleanup is done regardless of success/failure of drop
if (draggedItemElement) {
draggedItemElement.classList.remove('dragging');
}
// Clear all visual drag indicators from all sections/items
document.querySelectorAll(`#${checklistId} li.drag-over-before, #${checklistId} li.drag-over-after`).forEach(item => {
item.classList.remove('drag-over-before', 'drag-over-after');
});
document.querySelectorAll(`#${checklistId} ul.drag-over-empty`).forEach(ul => {
ul.classList.remove('drag-over-empty');
});
document.querySelectorAll(`#${checklistId} .task-section.drag-over-section, #${checklistId} .completed-section.drag-over-section`).forEach(sec => {
sec.classList.remove('drag-over-section');
});
draggedItemElement = null; // Clear the reference
}
// Called frequently when dragging over a potential drop target
function handleDragOver(e) {
e.preventDefault(); // Necessary to allow dropping
e.dataTransfer.dropEffect = 'move';
if (!draggedItemElement) return;
const targetSectionDiv = e.target.closest('.task-section, .completed-section');
if (!targetSectionDiv) return;
const targetList = targetSectionDiv.querySelector('ul[data-section-id]');
if (!targetList) return;
const targetItem = e.target.closest('li');
document.querySelectorAll(`#${checklistId} li.drag-over-before, #${checklistId} li.drag-over-after`).forEach(item => {
item.classList.remove('drag-over-before', 'drag-over-after');
});
document.querySelectorAll(`#${checklistId} ul.drag-over-empty`).forEach(ul => {
ul.classList.remove('drag-over-empty');
});
document.querySelectorAll(`#${checklistId} .task-section.drag-over-section, #${checklistId} .completed-section.drag-over-section`).forEach(sec => {
// Only remove if it's NOT the current target section
if (sec !== targetSectionDiv) {
sec.classList.remove('drag-over-section');
}
});
targetSectionDiv.classList.add('drag-over-section'); // Highlight the whole section
const isListEmpty = !targetList.querySelector('li:not(.dragging)');
if (targetItem && targetItem !== draggedItemElement && targetItem.parentNode === targetList) {
const targetRect = targetItem.getBoundingClientRect();
const isAfter = e.clientY > targetRect.top + targetRect.height / 2;
targetItem.classList.add(isAfter ? 'drag-over-after' : 'drag-over-before');
} else if (isListEmpty && !targetItem) {
targetList.classList.add('drag-over-empty');
}
}
// Called when leaving a potential drop target
function handleDragLeave(e) {
if (!e.relatedTarget || !e.relatedTarget.closest(`#${checklistId}`)) {
// Call handleDragEnd to perform a full cleanup if mouse leaves the list area
handleDragEnd(e);
} else {
const targetSectionDiv = e.target.closest('.task-section, .completed-section');
if (targetSectionDiv) {
const list = targetSectionDiv.querySelector('ul');
if (list && !list.contains(e.relatedTarget) && !targetSectionDiv.querySelector('.section-header').contains(e.relatedTarget)) {
list.classList.remove('drag-over-empty'); // Remove empty indicator if leaving list area but still in section
}
}
}
}
// Called when dropping an item
function handleDrop(e) {
e.preventDefault();
e.stopPropagation(); // Prevent dropping on multiple nested elements if possible
const currentDraggedItemElement = draggedItemElement;
draggedItemElement = null;
if (!currentDraggedItemElement) {
handleDragEnd(e); // Clean up visuals if drop happens without a valid dragged item
return;
}
const targetSectionDiv = e.target.closest('.task-section, .completed-section');
if (!targetSectionDiv) {
console.warn("MWI Tasklist: Drop occurred outside a valid section.");
handleDragEnd(e);
return;
}
const targetListElement = targetSectionDiv.querySelector('ul[data-section-id]');
if (!targetListElement) {
console.error("MWI Tasklist: Could not find target list within section.");
handleDragEnd(e);
return;
}
const dropTargetElement = e.target.closest('li');
// --- Update Data ---
const targetSectionId = targetListElement.dataset.sectionId;
const draggedItemId = currentDraggedItemElement.dataset.itemId;
const allItems = getChecklistItems();
const draggedItemDataIndex = findActualItemIndexById(draggedItemId, allItems);
if (draggedItemDataIndex === -1) {
console.error("MWI Tasklist: Could not find dragged item data by ID:", draggedItemId);
handleDragEnd(e); // Clean up visuals
renderChecklist(); // Re-render to reset UI state
return;
}
const [movedItemData] = allItems.splice(draggedItemDataIndex, 1);
const isDroppingOnCompleted = targetSectionId === '__completed__';
movedItemData.sectionId = isDroppingOnCompleted ? (movedItemData.sectionId || GLOBAL_SECTION_ID) : targetSectionId;
movedItemData.checked = isDroppingOnCompleted;
let finalInsertIndex = -1;
if (dropTargetElement && dropTargetElement !== currentDraggedItemElement && dropTargetElement.parentNode === targetListElement) {
const dropTargetItemId = dropTargetElement.dataset.itemId;
const dropTargetDataIndex = findActualItemIndexById(dropTargetItemId, allItems);
if (dropTargetDataIndex !== -1) {
const targetRect = dropTargetElement.getBoundingClientRect();
const isAfter = e.clientY > targetRect.top + targetRect.height / 2;
finalInsertIndex = isAfter ? dropTargetDataIndex + 1 : dropTargetDataIndex;
} else {
console.warn("MWI Tasklist: Could not find drop target item data:", dropTargetItemId);
}
}
if (finalInsertIndex === -1) {
if (isDroppingOnCompleted) {
let firstCompletedIndex = allItems.findIndex(item => item.checked);
finalInsertIndex = (firstCompletedIndex === -1) ? allItems.length : firstCompletedIndex;
} else {
let insertBeforeIndex = allItems.findIndex(item => item.checked || (item.sectionId || GLOBAL_SECTION_ID) !== targetSectionId);
finalInsertIndex = (insertBeforeIndex === -1) ? allItems.length : insertBeforeIndex;
}
}
finalInsertIndex = Math.max(0, Math.min(finalInsertIndex, allItems.length));
allItems.splice(finalInsertIndex, 0, movedItemData);
// --- Save and Re-render ---
setChecklistItems(allItems);
handleDragEnd(e);
renderChecklist();
}
// --- Initialization ---
function initialize() {
if (document.getElementById(checklistId)) {
console.log("MWI Tasklist: Already initialized.");
return;
}
console.log("MWI Tasklist: Initializing...");
createChecklistUI();
}
// --- Wait for Game Load ---
function waitForGameLoad() {
if (document.body) {
initialize();
} else {
setTimeout(waitForGameLoad, 300);
}
}
// --- Script Entry Point ---
waitForGameLoad();
})();