kanban-todo.txt/index.html
2025-12-30 21:17:55 +08:00

1100 lines
38 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KBTD</title>
<style>
:root {
--fg: #ffe;
--bg: #112;
--shade: #889;
--radius: 4px;
}
html {
font-size: 1.25rem;
box-sizing: border-box;
}
*, ::after, ::before {
box-sizing: inherit;
text-decoration-thickness: .1rem;
}
body {
margin: 0;
background: var(--bg);
color: var(--fg);
font-family: Helvetica, Arial, sans-serif;
padding: 1em;
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
a {
color: var(--fg);
}
button {
display: inline-block;
vertical-align: middle;
padding: .4rem .8rem;
font-size: 1rem;
font-weight: normal;
background: var(--fg);
color: var(--bg);
border: .1rem solid var(--fg);
border-radius: var(--radius);
cursor: pointer;
}
button:hover {
opacity: 0.9;
}
button:disabled {
cursor: not-allowed;
background: var(--shade);
border: .1rem solid var(--shade);
}
input[type="text"] {
appearance: none;
box-shadow: none;
box-sizing: inherit;
padding: .5rem .5rem;
width: 100%;
font-size: 1rem;
color: var(--fg);
background-color: var(--bg);
border: .1rem solid var(--fg);
border-radius: var(--radius);
font-family: Helvetica, Arial, sans-serif;
}
input::placeholder {
color: var(--fg);
font-style: italic;
opacity: 1;
}
h1 { font-size: 2em; }
h2 { font-size: 1.5em; }
h3 { font-size: 1.17em; }
h1, h2, h3 { margin-top: 0; }
/* Layouts */
#app {
flex-grow: 1;
display: flex;
flex-direction: column;
height: 100%;
position: relative;
}
.screen {
display: none;
flex-direction: column;
height: 100%;
width: 100%;
}
.screen.active {
display: flex;
}
.center-content {
justify-content: center;
align-items: center;
text-align: center;
gap: 1.5em;
}
/* Kanban Board */
header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 1em;
border-bottom: .1rem solid var(--shade);
margin-bottom: 1em;
flex-shrink: 0;
}
.header-controls {
display: flex;
gap: 0.5em;
align-items: center;
}
.board {
display: flex;
gap: 1em;
overflow-x: auto;
height: 100%;
align-items: flex-start;
padding-bottom: 1em;
}
.column {
background: transparent;
border-radius: var(--radius);
min-width: 200px;
max-width: 450px;
flex: 1 1 0;
display: flex;
flex-direction: column;
max-height: 100%;
border: .1rem solid var(--shade);
}
.column-header {
padding: 0.75em;
font-weight: bold;
border-bottom: .1rem solid var(--shade);
}
.card-list {
padding: 0.5em;
overflow-y: auto;
overflow-x: hidden;
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 0.5em;
min-height: 100px;
position: relative;
}
.card {
background: transparent;
padding: 0.75em;
border-radius: var(--radius);
border: .1rem solid var(--shade);
cursor: grab;
user-select: none;
word-wrap: break-word;
position: relative;
}
.card:hover {
border-color: var(--fg);
}
.card:active {
cursor: grabbing;
}
.card.completed {
text-decoration: line-through;
opacity: 0.6;
}
.card.dragging {
opacity: 0.5;
border: .1rem dashed var(--fg);
}
.card-checkbox {
float: right;
width: 1em;
height: 1em;
border: .1rem solid var(--fg);
border-radius: 2px;
cursor: pointer;
background: transparent;
margin-left: 0.5em;
margin-bottom: 0.5em;
}
.card-checkbox.checked {
background: var(--fg);
}
.drop-indicator {
height: 3px;
background: var(--fg);
border-radius: 2px;
opacity: 0;
transition: opacity 0.1s;
pointer-events: none;
}
.drop-indicator.active {
opacity: 1;
}
/* Section Picker */
.section-list {
list-style: none;
padding: 0;
width: 100%;
max-width: 600px;
display: flex;
flex-direction: column;
gap: 0.5em;
}
.section-btn {
width: 100%;
text-align: left;
padding: 1em;
background: transparent;
color: var(--fg);
border: .1rem solid var(--shade);
}
.section-btn:hover {
border-color: var(--fg);
}
.new-project-form {
width: 100%;
max-width: 600px;
display: flex;
gap: 0.5em;
margin-top: 1em;
}
.new-project-form input {
flex-grow: 1;
}
.error {
background-color: #782828;
padding: 0.5em 1em;
border-radius: var(--radius);
}
/* Scrollbar styling */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: var(--bg);
}
::-webkit-scrollbar-thumb {
background: var(--shade);
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--fg);
}
hr {
border: .05rem solid var(--fg);
}
p {
line-height: 1.4;
}
</style>
</head>
<body>
<div id="app">
<!-- Landing Screen -->
<div id="landing-screen" class="screen center-content">
<h1>KBTD</h1>
<p>Kanban TODO - Markdown based task management</p>
<p>To get started, open a Markdown file containing your tasks.</p>
<button id="open-file-btn">Open TODO.md</button>
<div id="landing-error" class="error" style="display:none"></div>
</div>
<!-- Permission Screen -->
<div id="permission-screen" class="screen center-content">
<h1>Permission Required</h1>
<p>KBTD needs permission to view and edit your file.</p>
<button id="grant-permission-btn">Grant Access</button>
</div>
<!-- Section Picker Screen -->
<div id="section-screen" class="screen center-content">
<h1>Select Project</h1>
<ul id="section-list" class="section-list"></ul>
<div class="new-project-form">
<input type="text" id="new-project-input" placeholder="New project name...">
<button id="create-project-btn">Create</button>
</div>
<button id="change-file-btn" style="margin-top: 1em">Change File</button>
</div>
<!-- Board Screen -->
<div id="board-screen" class="screen">
<header>
<h2 id="board-title" style="margin:0">Project Name</h2>
<div class="header-controls">
<button id="toggle-completed-btn">Show Completed</button>
<button id="back-btn">Projects</button>
</div>
</header>
<div id="board-container" class="board"></div>
</div>
</div>
<script>
// --- Constants & State ---
const DB_NAME = 'kbtd';
const DB_VERSION = 1;
const STORE_NAME = 'fileHandles';
let state = {
fileHandle: null,
filePath: null,
sections: [],
currentSectionIndex: -1,
lastModified: 0,
showCompleted: false
};
let dragState = {
source: null,
currentDropTarget: null
};
// --- IndexedDB Helper ---
const dbPromise = new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
});
async function getFileHandle(path) {
const db = await dbPromise;
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readonly');
const store = tx.objectStore(STORE_NAME);
const request = store.get(path);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function storeFileHandle(path, handle) {
const db = await dbPromise;
return new Promise((resolve, reject) => {
const tx = db.transaction(STORE_NAME, 'readwrite');
const store = tx.objectStore(STORE_NAME);
const request = store.put(handle, path);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
// --- Markdown Parsing ---
function parseMarkdown(text) {
const lines = text.split('\n');
const sections = [];
let currentSection = null;
let currentColumn = null;
let currentCard = null;
const finishCard = () => {
if (currentCard) {
if (currentColumn) {
currentColumn.cards.push(currentCard);
}
currentCard = null;
}
};
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('# ')) {
finishCard();
currentSection = {
name: line.substring(2).trim(),
columns: []
};
sections.push(currentSection);
currentColumn = null;
}
else if (line.startsWith('## ') && currentSection) {
finishCard();
currentColumn = {
name: line.substring(3).trim(),
headerLineNumber: i,
cards: []
};
currentSection.columns.push(currentColumn);
}
else if ((line.trim().startsWith('- [ ]') || line.trim().startsWith('- [x]')) && currentColumn) {
finishCard();
const isCompleted = line.trim().startsWith('- [x]');
const textStart = line.indexOf(']') + 2;
const text = line.substring(textStart).trim();
currentCard = {
text: text,
completed: isCompleted,
lineNumber: i,
originalLine: line,
nestedLines: []
};
}
else {
if (currentCard) {
currentCard.nestedLines.push(line);
}
}
}
finishCard();
return sections;
}
// --- File Operations ---
async function writeFile(text) {
try {
const writable = await state.fileHandle.createWritable();
await writable.write(text);
await writable.close();
console.log('File written successfully');
const file = await state.fileHandle.getFile();
state.lastModified = file.lastModified;
reloadFromText(text);
} catch (err) {
console.error('Error writing file:', err);
alert('Error saving file.');
}
}
function reloadFromText(text) {
console.log('Reloading from text after write');
state.sections = parseMarkdown(text);
console.log('Parsed sections:', state.sections);
if (state.currentSectionIndex !== -1 && state.sections[state.currentSectionIndex]) {
renderBoard();
} else {
renderSectionPicker();
}
}
async function updateLineInFile(lineNum, newText) {
console.log('updateLineInFile:', lineNum, newText);
const file = await state.fileHandle.getFile();
const text = await file.text();
const lines = text.split('\n');
lines[lineNum] = newText;
await writeFile(lines.join('\n'));
}
async function moveLineInFile(fromLine, toLine, targetColumnIndex, insertPosition) {
console.log('moveLineInFile: from line', fromLine, 'to line', toLine, 'targetColumnIndex:', targetColumnIndex, 'insertPosition:', insertPosition);
const file = await state.fileHandle.getFile();
const text = await file.text();
const lines = text.split('\n');
const section = state.sections[state.currentSectionIndex];
const targetColumn = section.columns[targetColumnIndex];
const targetColumnIsEmpty = targetColumn.cards.length === 0;
const isInsertingAtBottom = insertPosition === targetColumn.cards.length;
console.log('Before move, source line', fromLine, '=', lines[fromLine]);
console.log('Target column is empty:', targetColumnIsEmpty);
console.log('Inserting at bottom:', isInsertingAtBottom);
const [movedLine] = lines.splice(fromLine, 1);
let actualInsertLine;
if (targetColumnIsEmpty) {
console.log('Moving to empty column - inserting after header');
actualInsertLine = targetColumn.headerLineNumber + 1;
if (actualInsertLine < lines.length && lines[actualInsertLine].trim() === '') {
lines.splice(actualInsertLine, 0, movedLine);
} else {
lines.splice(actualInsertLine, 0, '', movedLine);
}
const afterLine = actualInsertLine + (lines[actualInsertLine].trim() === '' ? 2 : 1);
if (afterLine >= lines.length || lines[afterLine].trim() !== '') {
lines.splice(afterLine, 0, '');
}
} else {
if (insertPosition === 0) {
console.log('Inserting at start of column');
actualInsertLine = targetColumn.headerLineNumber + 1;
if (actualInsertLine < lines.length && lines[actualInsertLine].trim() === '') {
actualInsertLine++;
}
lines.splice(actualInsertLine, 0, movedLine);
} else {
console.log('Inserting at position', insertPosition);
const cardBefore = targetColumn.cards[insertPosition - 1];
actualInsertLine = cardBefore.lineNumber + 1;
if (cardBefore.nestedLines.length > 0) {
actualInsertLine += cardBefore.nestedLines.length;
}
if (fromLine < actualInsertLine) {
actualInsertLine--;
}
if (actualInsertLine > 0 && lines[actualInsertLine - 1].trim() === '') {
console.log('Line before target is blank, replacing it');
lines[actualInsertLine - 1] = movedLine;
} else {
console.log('Inserting at target position');
lines.splice(actualInsertLine, 0, movedLine);
}
if (isInsertingAtBottom) {
const movedLineIndex = lines[actualInsertLine - 1] === movedLine ? actualInsertLine - 1 : actualInsertLine;
const afterLine = movedLineIndex + 1;
if (afterLine >= lines.length || lines[afterLine].trim() !== '') {
console.log('Inserting at bottom - ensuring blank line after moved item at line', afterLine);
lines.splice(afterLine, 0, '');
}
}
}
}
console.log('After move, lines around target:');
for (let i = Math.max(0, actualInsertLine - 2); i <= Math.min(lines.length - 1, actualInsertLine + 2); i++) {
console.log(` line ${i}: "${lines[i]}"`);
}
await writeFile(lines.join('\n'));
}
async function createProject(projectName) {
console.log('Creating project:', projectName);
const file = await state.fileHandle.getFile();
const text = await file.text();
let newText;
if (text.trim() === '') {
newText = `# ${projectName}\n\n## TODO\n\n## In Progress\n\n## Done\n`;
} else {
newText = text + (text.endsWith('\n') ? '' : '\n') + `\n# ${projectName}\n\n## TODO\n\n## In Progress\n\n## Done\n`;
}
await writeFile(newText);
const newSectionIndex = state.sections.findIndex(s => s.name === projectName);
if (newSectionIndex !== -1) {
await selectSection(newSectionIndex);
}
}
async function loadFile() {
try {
const file = await state.fileHandle.getFile();
state.lastModified = file.lastModified;
const text = await file.text();
state.sections = parseMarkdown(text);
console.log('Parsed sections:', state.sections);
if (state.filePath) {
const key = `kbtd:${state.filePath}`;
const stored = localStorage.getItem(key);
if (stored) {
try {
const uiState = JSON.parse(stored);
if (state.currentSectionIndex === -1) {
const idx = state.sections.findIndex(s => s.name === uiState.currentSection);
if (idx !== -1) state.currentSectionIndex = idx;
}
if (uiState.showCompleted !== undefined) {
state.showCompleted = uiState.showCompleted;
}
} catch (e) { console.error(e); }
}
}
if (state.currentSectionIndex !== -1 && state.sections[state.currentSectionIndex]) {
renderBoard();
} else {
renderSectionPicker();
}
} catch (err) {
console.error('Error loading file:', err);
alert('Error loading file. See console.');
}
}
async function checkPermission(handle) {
const opts = { mode: 'readwrite' };
if ((await handle.queryPermission(opts)) === 'granted') {
return true;
}
if ((await handle.requestPermission(opts)) === 'granted') {
return true;
}
return false;
}
// --- UI Rendering ---
function showScreen(screenId) {
document.querySelectorAll('.screen').forEach(el => el.classList.remove('active'));
document.getElementById(screenId).classList.add('active');
}
function renderSectionPicker() {
const list = document.getElementById('section-list');
list.innerHTML = '';
state.sections.forEach((section, index) => {
const li = document.createElement('li');
const btn = document.createElement('button');
btn.className = 'section-btn';
btn.textContent = section.name;
btn.onclick = () => selectSection(index);
li.appendChild(btn);
list.appendChild(li);
});
document.getElementById('new-project-input').value = '';
showScreen('section-screen');
}
function renderBoard() {
const section = state.sections[state.currentSectionIndex];
if (!section) return renderSectionPicker();
document.getElementById('board-title').textContent = section.name;
const toggleBtn = document.getElementById('toggle-completed-btn');
toggleBtn.textContent = state.showCompleted ? 'Hide Completed' : 'Show Completed';
const container = document.getElementById('board-container');
container.innerHTML = '';
section.columns.forEach((column, colIndex) => {
const colDiv = document.createElement('div');
colDiv.className = 'column';
colDiv.dataset.colIndex = colIndex;
const header = document.createElement('div');
header.className = 'column-header';
header.textContent = column.name;
colDiv.appendChild(header);
const cardList = document.createElement('div');
cardList.className = 'card-list';
cardList.dataset.colIndex = colIndex;
cardList.ondragover = (e) => handleDragOver(e, colIndex, cardList);
cardList.ondrop = (e) => handleDrop(e, colIndex);
cardList.ondragleave = handleDragLeave;
const visibleCards = column.cards.filter((card, idx) => {
if (!state.showCompleted && card.completed) {
return false;
}
return true;
});
if (visibleCards.length > 0) {
const firstIndicator = document.createElement('div');
firstIndicator.className = 'drop-indicator';
firstIndicator.dataset.colIndex = colIndex;
firstIndicator.dataset.position = 0;
cardList.appendChild(firstIndicator);
}
column.cards.forEach((card, cardIndex) => {
if (!state.showCompleted && card.completed) {
return;
}
const cardDiv = document.createElement('div');
cardDiv.className = `card ${card.completed ? 'completed' : ''}`;
cardDiv.draggable = true;
cardDiv.dataset.lineNumber = card.lineNumber;
cardDiv.dataset.originalLine = card.originalLine;
cardDiv.dataset.colIndex = colIndex;
cardDiv.dataset.cardIndex = cardIndex;
const checkbox = document.createElement('div');
checkbox.className = `card-checkbox ${card.completed ? 'checked' : ''}`;
checkbox.onclick = async (e) => {
e.stopPropagation();
const lineNum = parseInt(cardDiv.dataset.lineNumber);
const originalLine = cardDiv.dataset.originalLine;
const newLine = originalLine.includes('- [x]')
? originalLine.replace('- [x]', '- [ ]')
: originalLine.replace('- [ ]', '- [x]');
await updateLineInFile(lineNum, newLine);
};
cardDiv.appendChild(checkbox);
const textSpan = document.createElement('span');
textSpan.textContent = card.text;
cardDiv.appendChild(textSpan);
cardDiv.ondragstart = (e) => handleDragStart(e, cardDiv);
cardDiv.ondragend = handleDragEnd;
cardDiv.ondragenter = (e) => handleCardDragEnter(e, cardDiv, colIndex, cardIndex);
cardList.appendChild(cardDiv);
const indicator = document.createElement('div');
indicator.className = 'drop-indicator';
indicator.dataset.colIndex = colIndex;
indicator.dataset.position = cardIndex + 1;
cardList.appendChild(indicator);
});
if (visibleCards.length === 0) {
const indicator = document.createElement('div');
indicator.className = 'drop-indicator';
indicator.dataset.colIndex = colIndex;
indicator.dataset.position = 0;
cardList.appendChild(indicator);
}
colDiv.appendChild(cardList);
container.appendChild(colDiv);
});
showScreen('board-screen');
}
// --- Actions ---
async function selectSection(index) {
state.currentSectionIndex = index;
if (state.filePath) {
const key = `kbtd:${state.filePath}`;
const uiState = {
currentSection: state.sections[index].name,
showCompleted: state.showCompleted
};
localStorage.setItem(key, JSON.stringify(uiState));
}
renderBoard();
}
function toggleShowCompleted() {
state.showCompleted = !state.showCompleted;
if (state.filePath) {
const key = `kbtd:${state.filePath}`;
const stored = localStorage.getItem(key);
let uiState = {};
if (stored) {
try {
uiState = JSON.parse(stored);
} catch (e) { console.error(e); }
}
uiState.showCompleted = state.showCompleted;
localStorage.setItem(key, JSON.stringify(uiState));
}
renderBoard();
}
// --- Drag and Drop ---
function handleDragStart(e, cardDiv) {
dragState.source = {
lineNumber: parseInt(cardDiv.dataset.lineNumber),
colIndex: parseInt(cardDiv.dataset.colIndex),
cardIndex: parseInt(cardDiv.dataset.cardIndex)
};
e.target.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', JSON.stringify(dragState.source));
console.log('Drag started:', dragState.source);
}
function handleDragEnd(e) {
e.target.classList.remove('dragging');
clearDropIndicators();
dragState.source = null;
dragState.currentDropTarget = null;
}
function clearDropIndicators() {
document.querySelectorAll('.drop-indicator').forEach(el => {
el.classList.remove('active');
});
}
function handleCardDragEnter(e, cardDiv, colIndex, cardIndex) {
if (!dragState.source) return;
e.preventDefault();
e.stopPropagation();
const rect = cardDiv.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
const mouseY = e.clientY;
let position;
if (mouseY < midpoint) {
position = cardIndex;
} else {
position = cardIndex + 1;
}
console.log('Card drag enter: col', colIndex, 'position', position);
dragState.currentDropTarget = { colIndex, position };
clearDropIndicators();
const indicator = document.querySelector(`.drop-indicator[data-col-index="${colIndex}"][data-position="${position}"]`);
if (indicator) {
indicator.classList.add('active');
}
}
function handleDragOver(e, colIndex, cardList) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (!dragState.source) return;
const cards = Array.from(cardList.querySelectorAll('.card'));
if (cards.length === 0) {
console.log('Drag over empty column', colIndex);
dragState.currentDropTarget = { colIndex, position: 0 };
clearDropIndicators();
const indicator = cardList.querySelector('.drop-indicator');
if (indicator) {
indicator.classList.add('active');
}
return;
}
let closestCard = null;
let closestDistance = Infinity;
let insertBefore = true;
cards.forEach(card => {
const rect = card.getBoundingClientRect();
const midpoint = rect.top + rect.height / 2;
const distance = Math.abs(e.clientY - midpoint);
if (distance < closestDistance) {
closestDistance = distance;
closestCard = card;
insertBefore = e.clientY < midpoint;
}
});
if (closestCard) {
const cardIndex = parseInt(closestCard.dataset.cardIndex);
const position = insertBefore ? cardIndex : cardIndex + 1;
if (!dragState.currentDropTarget ||
dragState.currentDropTarget.colIndex !== colIndex ||
dragState.currentDropTarget.position !== position) {
console.log('Drag over: col', colIndex, 'position', position);
dragState.currentDropTarget = { colIndex, position };
clearDropIndicators();
const indicator = document.querySelector(`.drop-indicator[data-col-index="${colIndex}"][data-position="${position}"]`);
if (indicator) {
indicator.classList.add('active');
}
}
}
}
function handleDragLeave(e) {
if (e.target.classList.contains('card-list')) {
const relatedTarget = e.relatedTarget;
if (!relatedTarget || !e.currentTarget.contains(relatedTarget)) {
console.log('Drag leave card-list');
}
}
}
async function handleDrop(e, targetColIndex) {
e.stopPropagation();
e.preventDefault();
clearDropIndicators();
if (!dragState.source || !dragState.currentDropTarget) {
console.log('Drop ignored: no source or target');
return;
}
const sourceColIndex = dragState.source.colIndex;
const sourceLineNumber = dragState.source.lineNumber;
const targetPosition = dragState.currentDropTarget.position;
console.log('Drop: sourceCol', sourceColIndex, 'targetCol', targetColIndex, 'position', targetPosition);
if (sourceColIndex === targetColIndex) {
const sourceCardIndex = dragState.source.cardIndex;
if (targetPosition === sourceCardIndex || targetPosition === sourceCardIndex + 1) {
console.log('Same position, ignoring drop');
dragState.source = null;
dragState.currentDropTarget = null;
return;
}
}
const section = state.sections[state.currentSectionIndex];
const targetColumn = section.columns[targetColIndex];
console.log('Target column:', targetColumn.name, 'position:', targetPosition);
await moveLineInFile(sourceLineNumber, 0, targetColIndex, targetPosition);
dragState.source = null;
dragState.currentDropTarget = null;
return false;
}
// --- Initialization & Polling ---
async function init() {
const params = new URLSearchParams(window.location.search);
state.filePath = params.get('file');
if (!state.filePath) {
showScreen('landing-screen');
return;
}
try {
const handle = await getFileHandle(state.filePath);
if (handle) {
state.fileHandle = handle;
const perm = await handle.queryPermission({ mode: 'readwrite' });
if (perm === 'granted') {
await loadFile();
startPolling();
} else {
showScreen('permission-screen');
}
} else {
showScreen('landing-screen');
document.getElementById('landing-error').textContent =
`Please select the file: ${state.filePath}`;
document.getElementById('landing-error').style.display = 'block';
}
} catch (err) {
console.error(err);
}
}
function startPolling() {
setInterval(async () => {
if (!state.fileHandle) return;
try {
const file = await state.fileHandle.getFile();
if (file.lastModified > state.lastModified) {
console.log('File changed externally, reloading...');
await loadFile();
}
} catch (e) {
console.error('Polling error:', e);
}
}, 250);
}
// --- Event Listeners ---
document.getElementById('open-file-btn').onclick = async () => {
try {
const [handle] = await window.showOpenFilePicker({
id: 'kbtd-todo-file',
mode: 'readwrite',
types: [{
description: 'Markdown files',
accept: { 'text/markdown': ['.md'] }
}]
});
state.fileHandle = handle;
if (state.filePath) {
await storeFileHandle(state.filePath, handle);
}
await loadFile();
startPolling();
} catch (err) {
if (err.name !== 'AbortError') {
console.error(err);
alert('Error opening file');
}
}
};
document.getElementById('grant-permission-btn').onclick = async () => {
if (state.fileHandle) {
if (await checkPermission(state.fileHandle)) {
await loadFile();
startPolling();
}
}
};
document.getElementById('back-btn').onclick = () => {
state.currentSectionIndex = -1;
renderSectionPicker();
};
document.getElementById('toggle-completed-btn').onclick = () => {
toggleShowCompleted();
};
document.getElementById('change-file-btn').onclick = () => {
document.getElementById('open-file-btn').click();
};
document.getElementById('create-project-btn').onclick = async () => {
const input = document.getElementById('new-project-input');
const projectName = input.value.trim();
if (!projectName) {
alert('Please enter a project name');
return;
}
if (state.sections.some(s => s.name === projectName)) {
alert('A project with this name already exists');
return;
}
await createProject(projectName);
};
document.getElementById('new-project-input').onkeypress = (e) => {
if (e.key === 'Enter') {
document.getElementById('create-project-btn').click();
}
};
init();
</script>
</body>
</html>