Fanfiction Outline Generator

Fanfiction Outline Generator

1. Story Metadata
2. Plot Structure

Add New Plot Structure

Add New Structure

Available Plot Structures

Name Key Points (Preview) Actions

Fill out the Builder tab and click "Generate Outline Draft" to preview your story structure.

Select a structure to begin outlining.

'; return; } structure.points.forEach((point, index) => { const container = document.createElement('div'); container.className = 'ff-form-group full-width plot-point-group'; container.innerHTML = ` `; plotPointsContainer.appendChild(container); }); }; // --- Config Tab Functions (CRUD for Structures) --- const populateStructureDropdown = () => { plotStructureSelect.innerHTML = ''; if (structures.length === 0) { plotStructureSelect.innerHTML = ''; return; } structures.forEach(struct => { const option = new Option(struct.name, struct.id); plotStructureSelect.appendChild(option); }); loadStructure(); // Re-load inputs for the selected structure }; const renderStructureList = () => { structureListBody.innerHTML = ''; structures.forEach((struct) => { const pointsPreview = struct.points.slice(0, 3).join('; ') + (struct.points.length > 3 ? '...' : ''); const row = document.createElement('tr'); row.innerHTML = ` ${struct.name} ${pointsPreview} `; structureListBody.appendChild(row); }); populateStructureDropdown(); }; structureForm.addEventListener('submit', (e) => { e.preventDefault(); const isEditing = !!structEditIdInput.value; const currentId = structEditIdInput.value || `struct_${Date.now()}`; const pointsText = document.getElementById('struct-points').value; const newPoints = pointsText.split('\n').map(p => p.trim()).filter(p => p.length > 0); if (newPoints.length === 0) { alert("Please enter at least one plot point label."); return; } const newStruct = { id: currentId, name: document.getElementById('struct-name').value, points: newPoints }; if (isEditing) { const index = structures.findIndex(t => t.id === currentId); if (index !== -1) { structures[index] = newStruct; } } else { structures.push(newStruct); } // Reset form state structureForm.reset(); structEditIdInput.value = ''; document.getElementById('struct-legend').textContent = 'Add New Structure'; structAddUpdateBtn.textContent = 'Add Structure'; renderStructureList(); }); window.editStructure = (id) => { const struct = structures.find(t => t.id === id); if (struct) { structEditIdInput.value = struct.id; document.getElementById('struct-name').value = struct.name; document.getElementById('struct-points').value = struct.points.join('\n'); document.getElementById('struct-legend').textContent = `Editing Structure: ${struct.name}`; structAddUpdateBtn.textContent = 'Update Structure'; switchTab(1); // Switch to config tab } }; window.deleteStructure = (id) => { if (confirm('Are you sure you want to delete this plot structure?')) { structures = structures.filter(t => t.id !== id); renderStructureList(); } }; // --- Outline Generation (Tab 3) --- const getOutlineData = () => { const metadata = getMetadata(); const selectedId = plotStructureSelect.value; const structure = structures.find(s => s.id === selectedId); const plotPoints = []; if (structure) { structure.points.forEach((point, index) => { const textarea = document.getElementById(`plot-point-${index}`); if (textarea) { plotPoints.push({ label: point, content: textarea.value }); } }); } return { metadata, structure: structure ? structure.name : 'None Selected', plotPoints }; }; const generateOutline = () => { const data = getOutlineData(); reviewContent.innerHTML = ''; if (!data.structure || data.plotPoints.length === 0) { reviewContent.innerHTML = '

Please select a structure and fill out the plot points in the Builder tab.

'; pdfDownloadBtn.disabled = true; return; } // --- Metadata --- let html = `

${data.structure} Outline

`; // --- Plot Points --- data.plotPoints.forEach((point, index) => { html += `

${index + 1}. ${point.label}

${point.content || '(Plot point details missing)'}

`; }); reviewContent.innerHTML = html; pdfDownloadBtn.disabled = false; switchTab(2); // Switch to review tab }; /** * PDF Generation Function (Fully Functional) */ const downloadPDF = () => { const data = getOutlineData(); const jsPDF = window.jspdf.jsPDF; const doc = new jsPDF('p', 'pt', 'a4'); let currentY = 40; const margin = 40; const pageWidth = doc.internal.pageSize.width; const maxWidth = pageWidth - (margin * 2); const checkPageBreak = (spaceNeeded) => { if (currentY + spaceNeeded > doc.internal.pageSize.height - margin) { doc.addPage(); currentY = margin; } }; const addText = (text, size = 10, style = 'normal', indent = 0) => { doc.setFontSize(size); doc.setFont('Helvetica', style); const lines = doc.splitTextToSize(text, maxWidth - indent); checkPageBreak(lines.length * (size * 1.2)); doc.text(lines, margin + indent, currentY); currentY += (lines.length * (size * 1.2)); }; // --- PDF Content --- // Title Block doc.setFontSize(22); doc.setFont('Helvetica', 'bold'); doc.setTextColor(106, 90, 205); doc.text(data.metadata.title.toUpperCase() || 'UNTITLED FANFICTION', pageWidth / 2, currentY, { align: 'center' }); currentY += 15; doc.setFontSize(12); doc.setTextColor(74, 78, 105); addText(`Fandom: ${data.metadata.fandom || 'N/A'} | Pairing/MC: ${data.metadata.pairing || 'N/A'} | Genre: ${data.metadata.genre || 'N/A'}`, 12, 'normal', 0); currentY += 5; doc.setFontSize(10); addText(`Premise: ${data.metadata.summary || 'No summary provided.'}`, 10, 'italic', 0); currentY += 20; // Outline Header doc.setFontSize(16); doc.setFont('Helvetica', 'bold'); doc.setTextColor(106, 90, 205); doc.text(`${data.structure} Outline`, margin, currentY); currentY += 5; doc.setLineWidth(0.5); doc.setDrawColor(200); doc.line(margin, currentY, pageWidth - margin, currentY); currentY += 15; doc.setTextColor(0); // Plot Points data.plotPoints.forEach((point, index) => { checkPageBreak(30); // Point Label (Header) doc.setFontSize(12); doc.setFont('Helvetica', 'bold'); doc.setTextColor(74, 78, 105); addText(`${index + 1}. ${point.label}`, 12, 'bold', 0); // Point Content doc.setFontSize(11); doc.setFont('Helvetica', 'normal'); addText(point.content || '(Plot point details missing)', 11, 'normal', 10); currentY += 5; }); doc.save(`${data.metadata.title.replace(/\s/g, '_') || 'Fanfiction'}_Outline.pdf`); }; // --- Event Listeners and Initial Load --- plotStructureSelect.addEventListener('change', loadStructure); generateOutlineBtn.addEventListener('click', generateOutline); pdfDownloadBtn.addEventListener('click', downloadPDF); // Tab Navigation const switchTab = (tabIndex) => { tabs.forEach((tab, index) => { tab.classList.toggle('active', index === tabIndex); contents[index].classList.toggle('active', index === tabIndex); }); currentTab = tabIndex; updateNavButtons(); if (tabIndex === 2) { generateOutline(); } else if (tabIndex === 1) { renderStructureList(); } }; const updateNavButtons = () => { prevBtn.disabled = currentTab === 0; nextBtn.disabled = currentTab === tabs.length - 1; }; tabs.forEach((tab, index) => { tab.addEventListener('click', () => { const tabNode = tab.closest('.ff-tab-button'); const newIndex = Array.from(tabNode.parentNode.children).indexOf(tabNode); switchTab(newIndex); }); }); nextBtn.addEventListener('click', () => { if (currentTab < tabs.length - 1) switchTab(currentTab + 1); }); prevBtn.addEventListener('click', () => { if (currentTab > 0) switchTab(currentTab - 1); }); // Initial Setup renderStructureList(); updateNavButtons(); });
Scroll to Top