```` <%* // Prompt for adventure name let adventureName = await tp.system.prompt("Enter Adventure Name:"); if (!adventureName || adventureName.trim() === "") { new Notice("Adventure creation canceled."); return; } // **Sanitize and trim adventure name** adventureName = adventureName.trim().replace(/[<>:"/\\|?*]/g, ''); // Remove invalid filename characters // Define paths const basePath = `Adventures/${adventureName}`; const sessionNotesPath = `${basePath}/Session Notes`; const newFilePath = `${basePath}/${adventureName} - Adventure Hub`; let sessionTemplatePath = `Extras/Templates/(${adventureName})sessiontemplate`; // We'll add ".md" later const baseSessionTemplate = "[[Extras/Templates/base_sessiontemplate.md]]"; // Obsidian link // Ensure required folders exist const foldersToCreate = [ basePath, `${basePath}/Adventure`, `${basePath}/Materials`, sessionNotesPath ]; for (const folder of foldersToCreate) { if (!(await app.vault.adapter.exists(folder))) { await app.vault.createFolder(folder); } } // Small delay to ensure folders exist before file creation await new Promise(resolve => setTimeout(resolve, 500)); // --- Build the Adventure Hub Note --- let frontmatter = `--- players: [] adventure: "${adventureName}" ---`; let noteContent = `${frontmatter} # 📜 ${adventureName} - Adventure Hub ## 📚 Chapter Links \`\`\`dataviewjs const currentFolder = dv.current().file.folder; const adventureFolder = \`\${currentFolder}/Adventure\`; const pages = dv.pages(\`"\${adventureFolder}"\`).sort(p => p.file.name); if (pages.length === 0) { dv.paragraph("You haven't written anything yet."); } else { pages.forEach(p => dv.header(4, p.file.link)); } \`\`\` --- >[!success] Current Session: \`\`\`dataviewjs const pages = dv.pages('"Adventures/${adventureName}/Session Notes"') .sort(p => p.file.ctime, 'desc') .limit(1); if (pages.length) { dv.header(4, pages[0].file.link); } \`\`\` \`\`\`meta-bind-button label: Create first session icon: "" hidden: false class: "" tooltip: "Create your first session note" id: first-session style: primary actions: - type: templaterCreateNote templateFile: "Extras/Templates/(${adventureName})sessiontemplate.md" folderPath: "Adventures/${adventureName}/Session Notes" fileName: "" openNote: true \`\`\` --- ## 👥 Player Characters \`\`\`dataviewjs const container = dv.container; container.style.display = "flex"; container.style.flexDirection = "column"; container.style.gap = "10px"; container.style.marginBottom = "20px"; // Create button container for better alignment const buttonContainer = document.createElement("div"); buttonContainer.style.display = "flex"; buttonContainer.style.gap = "10px"; // Spacing between buttons buttonContainer.style.alignItems = "center"; // Align buttons properly buttonContainer.style.justifyContent = "flex-start"; // Align to left // Fetch player character notes from both folders let playerCharacters = dv .pages('"World/People/Player Characters/Active"') .concat(dv.pages('"World/People/Player Characters/Inactive"')) .map(p => p.file.name) .sort(); // Create dropdown button const dropdownButton = document.createElement("button"); dropdownButton.innerHTML = "Select Player Characters"; dropdownButton.style.padding = "6px 12px"; dropdownButton.style.border = "1px solid #444"; dropdownButton.style.borderRadius = "4px"; dropdownButton.style.cursor = "pointer"; dropdownButton.style.backgroundColor = "#444"; dropdownButton.style.color = "#fff"; dropdownButton.style.minWidth = "180px"; // Set fixed width for consistency dropdownButton.style.textAlign = "center"; // Create dropdown content (hidden initially) const dropdownContent = document.createElement("div"); dropdownContent.style.display = "none"; dropdownContent.style.position = "absolute"; dropdownContent.style.backgroundColor = "#333"; dropdownContent.style.border = "1px solid #444"; dropdownContent.style.borderRadius = "4px"; dropdownContent.style.padding = "10px"; dropdownContent.style.width = "200px"; dropdownContent.style.zIndex = "9999"; dropdownContent.style.maxHeight = "200px"; dropdownContent.style.overflowY = "auto"; // Populate dropdown with checkboxes playerCharacters.forEach(character => { const label = document.createElement("label"); label.style.color = "#fff"; label.style.display = "flex"; label.style.alignItems = "center"; label.style.padding = "5px"; label.style.cursor = "pointer"; const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.value = character; label.appendChild(checkbox); label.appendChild(document.createTextNode(" " + character)); // Hover effect label.addEventListener("mouseover", () => (label.style.backgroundColor = "#555")); label.addEventListener("mouseout", () => (label.style.backgroundColor = "transparent")); dropdownContent.appendChild(label); }); // Append dropdown to BODY to prevent clipping document.body.appendChild(dropdownContent); // Toggle dropdown visibility and position it correctly dropdownButton.addEventListener("click", function () { if (dropdownContent.style.display === "none") { dropdownContent.style.display = "block"; const rect = dropdownButton.getBoundingClientRect(); dropdownContent.style.left = rect.left + "px"; dropdownContent.style.top = rect.bottom + window.scrollY + "px"; } else { dropdownContent.style.display = "none"; } }); // Close dropdown when clicking outside document.addEventListener("click", function (event) { if (!dropdownButton.contains(event.target) && !dropdownContent.contains(event.target)) { dropdownContent.style.display = "none"; } }); // Create "Add" button const addCharacterButton = document.createElement("button"); addCharacterButton.innerHTML = "Add"; addCharacterButton.style.padding = "6px 12px"; addCharacterButton.style.border = "1px solid #444"; addCharacterButton.style.borderRadius = "4px"; addCharacterButton.style.cursor = "pointer"; addCharacterButton.style.backgroundColor = "#444"; addCharacterButton.style.color = "#fff"; addCharacterButton.style.minWidth = "80px"; addCharacterButton.style.textAlign = "center"; // Modify "Add" button to update metadata addCharacterButton.addEventListener("click", async () => { const selectedCharacters = Array.from(dropdownContent.querySelectorAll("input:checked")) .map(cb => "[[" + cb.value + "]]"); if (selectedCharacters.length === 0) { new Notice("Please select at least one player character."); return; } const file = app.workspace.getActiveFile(); if (!file) { new Notice("No active file detected."); return; } await app.fileManager.processFrontMatter(file, frontmatter => { if (!frontmatter.players) { frontmatter.players = []; } if (!Array.isArray(frontmatter.players)) { frontmatter.players = [frontmatter.players]; } frontmatter.players = [...new Set([...frontmatter.players, ...selectedCharacters])]; }); new Notice("Updated players: " + selectedCharacters.join(", ")); }); // Add buttons above the table buttonContainer.appendChild(dropdownButton); buttonContainer.appendChild(addCharacterButton); container.appendChild(buttonContainer); // --- Existing Player Table --- const players = dv.current().players; if (!players || players.length === 0) { dv.paragraph("No players found."); } else { const playerPages = players .map(player => dv.page(player)) .filter(p => p); dv.table( ["Player", "Species", "Class", "Level", "Max HP", "AC", "STR", "DEX", "CON", "INT", "WIS", "CHA"], playerPages.map(p => [ p.file.link, p.Species, p.Class, p.level, p.hp, p.ac, p.Strength, p.Dexterity, p.Constitution, p.Intelligence, p.Wisdom, p.Charisma ]) ); } \`\`\` --- ## ⏳ Takeaways \`\`\`dataviewjs const folderPath = "Adventures/${adventureName}/Session Notes"; const extractTakeaways = (content, startHeader, endHeader) => { const startPattern = new RegExp(\`^#+\\s*\${startHeader}\\s*\`, "im"); const endPattern = new RegExp(\`^#+\\s*\${endHeader}\\s*\`, "im"); const startMatch = content.match(startPattern); const endMatch = content.match(endPattern); if (!startMatch) return null; const startIndex = startMatch.index + startMatch[0].length; const endIndex = endMatch ? endMatch.index : content.length; return content.slice(startIndex, endIndex).replace(/^(#+.*|Notes last touched.*)$/gim, "").trim() || null; }; // Collect notes, extract takeaways, sort them, and build the table (async () => { let tableData = await Promise.all(dv.pages(\`"\${folderPath}"\`) .map(async page => { const content = await dv.io.load(page.file.path); const takeaway = content ? extractTakeaways(content, "Takeaways", "Next Session") : null; return takeaway ? [page.file.name, takeaway] : null; })); tableData = tableData.filter(row => row !== null); tableData.sort((a, b) => { // Remove optional chaining for compatibility: const matchA = a[0].match(/Session (\d+)/); const matchB = b[0].match(/Session (\d+)/); const sessionNumberA = parseInt(matchA ? matchA[1] : 0, 10); const sessionNumberB = parseInt(matchB ? matchB[1] : 0, 10); return sessionNumberB - sessionNumberA; }); tableData = tableData.slice(0, 5); if (tableData.length > 0) dv.table(["Note", "Takeaway"], tableData); })(); \`\`\` `; // === Now Copy the Base Session Template to Create the Session Template File === // Define the raw file path for the base template (no Obsidian link format) const baseTemplatePath = "Extras/Templates/base_sessiontemplate.md"; // Get the file object for the base session template. const baseTemplateFile = app.vault.getAbstractFileByPath(baseTemplatePath); if (!baseTemplateFile) { new Notice("Base session template file not found!"); return; } let baseSessionTemplateContent; try { // Read the raw content of the base template baseSessionTemplateContent = await app.vault.read(baseTemplateFile); } catch (error) { console.error("Failed to read base session template:", error); new Notice("Error: Could not read base session template."); return; } // Perform replacements on the raw content so that all placeholders are replaced let finalTemplateContent = baseSessionTemplateContent // Replace any occurrence of a placeholder variable with the actual adventure name. .replace(/\$\{adventureName\}/g, adventureName) .replace(/ADVENTURE FOLDER/g, adventureName) .replace(/SESSION NOTES FOLDER/g, "Session Notes") .replace(/\(exampleadventure\)/g, `(${adventureName})`); // Ensure the session template path ends with ".md" if (!sessionTemplatePath.endsWith('.md')) { sessionTemplatePath += '.md'; } try { // Create the new session template file with the processed content. await app.vault.create(sessionTemplatePath, finalTemplateContent); new Notice(`Session template for "${adventureName}" created!`); } catch (error) { console.error("Error creating session template:", error); new Notice("Error: Could not create session template."); } // Create a summary file in the Extras/SYS folder let summaryFilePath = `Extras/SYS/${adventureName}_summary.md`; let summaryContent = ``; try { await app.vault.create(summaryFilePath, summaryContent); new Notice(`Summary file created: ${summaryFilePath}`); } catch (error) { console.error("Error creating summary file:", error); new Notice("Error: Could not create summary file."); } // --- Create the Adventure Hub Note --- await tp.file.create_new(noteContent, newFilePath); await app.workspace.openLinkText(newFilePath, '', true); // Notify the user new Notice(`Adventure "${adventureName}" and session template created!`); %>