Created
September 14, 2024 12:51
-
-
Save josh33/1a0ba782312ca2df8d60a6d8a1f69f10 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Variables used by Scriptable. | |
| // These must be at the very top of the file. Do not edit. | |
| // icon-color: deep-purple; icon-glyph: tasks; | |
| const vaultName = "vaultname" // Replace with your actual vault name added as a file bookmark in Obsidian. Have not tested with iCloud syncing vaults. | |
| // const excludeFolders = ["200 Spiritual/_Scriptures", "_Attachments", "_Templates"] | |
| const excludeFolders = ["comma","delimited"] // if you have any large folders that will never have active tasks, add them here. | |
| const lightColors = { | |
| background: "#FFFFFF", | |
| text: "#000000", | |
| accent: "#007AFF", | |
| pill: "#E0E0E0", | |
| pillText: "#000000", | |
| circle: "#C7C7CC" | |
| } | |
| const darkColors = { | |
| background: "#000000", | |
| text: "#FFFFFF", | |
| accent: "#0A84FF", | |
| pill: "#2C2C2E", | |
| pillText: "#FFFFFF", | |
| circle: "#48484A" | |
| } | |
| function getColors() { | |
| return Device.isUsingDarkAppearance() ? darkColors : lightColors | |
| } | |
| // Function to get all markdown files recursively | |
| async function getMarkdownFiles(dir) { | |
| const files = [] | |
| const items = await FileManager.local().listContents(dir) | |
| for (const item of items) { | |
| const path = `${dir}/${item}` | |
| const isDirectory = await FileManager.local().isDirectory(path) | |
| if (isDirectory) { | |
| if (!excludeFolders.includes(item)) { | |
| files.push(...await getMarkdownFiles(path)) | |
| } | |
| } else if (item.endsWith('.md')) { | |
| files.push(path) | |
| } | |
| } | |
| return files | |
| } | |
| // Function to parse tasks from a file | |
| async function parseTasksFromFile(filePath) { | |
| const content = await FileManager.local().readString(filePath) | |
| const lines = content.split('\n') | |
| const tasks = [] | |
| const dateRegex = /(📅|🗓️|🛫|🛬|↓|🔁|📆)\s*(\d{4}-\d{2}-\d{2})/g | |
| const emojiMap = { | |
| '📅': 'Due', | |
| '🗓️': 'Scheduled', | |
| '🛫': 'Start', | |
| '🛬': 'End', | |
| '↓': 'Scheduled', | |
| '🔁': 'Repeat', | |
| '📆': 'Date' | |
| } | |
| for (const line of lines) { | |
| if (line.match(/^- \[ \]/)) { | |
| let taskText = line.replace(/^- \[ \] /, '').trim() | |
| let dates = {} | |
| const matches = [...taskText.matchAll(dateRegex)] | |
| for (const match of matches) { | |
| const [fullMatch, emoji, date] = match | |
| dates[emojiMap[emoji]] = date | |
| taskText = taskText.replace(fullMatch, '').trim() | |
| } | |
| const task = { | |
| text: taskText, | |
| dates: dates, | |
| filePath: filePath | |
| } | |
| tasks.push(task) | |
| } | |
| } | |
| return tasks | |
| } | |
| function getFileName(filePath) { | |
| const parts = filePath.split('/') | |
| return parts[parts.length - 1].replace('.md', '') | |
| } | |
| // Function to prioritize tasks | |
| function prioritizeTasks(tasks) { | |
| const now = new Date() | |
| now.setHours(0, 0, 0, 0) | |
| function getDateValue(task, dateType) { | |
| const date = task.dates[dateType] | |
| return date ? new Date(date) : null | |
| } | |
| function compareDates(dateA, dateB) { | |
| if (!dateA && !dateB) return 0 | |
| if (!dateA) return 1 | |
| if (!dateB) return -1 | |
| return dateA - dateB | |
| } | |
| return tasks.sort((a, b) => { | |
| // Sort by status (overdue, dueToday, scheduled, unplanned) | |
| const statusA = getTaskStatus(a, now) | |
| const statusB = getTaskStatus(b, now) | |
| if (statusA !== statusB) { | |
| return statusOrder.indexOf(statusA) - statusOrder.indexOf(statusB) | |
| } | |
| // Sort by due date | |
| const dueDateA = getDateValue(a, 'Due') | |
| const dueDateB = getDateValue(b, 'Due') | |
| const dueComparison = compareDates(dueDateA, dueDateB) | |
| if (dueComparison !== 0) return dueComparison | |
| // Sort by scheduled date | |
| const scheduledDateA = getDateValue(a, 'Scheduled') | |
| const scheduledDateB = getDateValue(b, 'Scheduled') | |
| const scheduledComparison = compareDates(scheduledDateA, scheduledDateB) | |
| if (scheduledComparison !== 0) return scheduledComparison | |
| // Sort by start date | |
| const startDateA = getDateValue(a, 'Start') | |
| const startDateB = getDateValue(b, 'Start') | |
| const startComparison = compareDates(startDateA, startDateB) | |
| if (startComparison !== 0) return startComparison | |
| // If all else is equal, sort by task text | |
| return a.text.localeCompare(b.text) | |
| }) | |
| } | |
| const statusOrder = ['overdue', 'dueToday', 'scheduled', 'unplanned'] | |
| function getTaskStatus(task, now) { | |
| const dueDate = task.dates['Due'] ? new Date(task.dates['Due'] + 'T00:00:00') : null | |
| const scheduledDate = task.dates['Scheduled'] ? new Date(task.dates['Scheduled'] + 'T00:00:00') : null | |
| if (dueDate) { | |
| if (dueDate < now) return 'overdue' | |
| if (dueDate.getTime() === now.getTime()) return 'dueToday' | |
| } | |
| if (scheduledDate) return 'scheduled' | |
| return 'unplanned' | |
| } | |
| // Function to classify tasks into "assigned to me" or "waiting on others" | |
| function classifyTasks(tasks) { | |
| const assignedToMe = [] | |
| const waitingOnOthers = [] | |
| for (const task of tasks) { | |
| const isAssignedToOthers = task.text.includes("[[@") || task.filePath.includes("/@") | |
| if (isAssignedToOthers) { | |
| waitingOnOthers.push(task) | |
| } else { | |
| assignedToMe.push(task) | |
| } | |
| } | |
| return { assignedToMe, waitingOnOthers } | |
| } | |
| // Function to extract the first name from a task or file | |
| function extractFirstName(task) { | |
| let match = task.text.match(/\[\[@(\w+)/) || task.filePath.match(/\/@(\w+)/) | |
| return match ? match[1] : "Unknown" | |
| } | |
| // Function to format the due date | |
| function formatDueDate(dueDate) { | |
| const now = new Date() | |
| now.setHours(0, 0, 0, 0) | |
| // Parse the date string and adjust for local timezone | |
| const due = new Date(dueDate + 'T00:00:00') | |
| let color | |
| let status | |
| let formattedDate | |
| const diffTime = Math.abs(due - now) | |
| const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) | |
| if (due < now) { | |
| color = new Color("#FF3B30") // Red for overdue | |
| status = 'past' | |
| formattedDate = diffDays === 1 ? "1 day ago" : `${diffDays} days ago` | |
| } else if (due.getTime() === now.getTime()) { | |
| color = new Color("#FF9500") // Orange for today | |
| status = 'today' | |
| formattedDate = "today" | |
| } else { | |
| color = new Color("#34C759") // Green for future dates | |
| status = 'future' | |
| formattedDate = diffDays === 1 ? "in 1 day" : `in ${diffDays} days` | |
| } | |
| return { text: formattedDate, color: color, status: status } | |
| } | |
| // Function to parse basic markdown links | |
| function parseMarkdownLinks(text) { | |
| const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g | |
| return text.replace(linkRegex, '$1') | |
| } | |
| // Function to load images | |
| function loadImage(imageName) { | |
| const fm = FileManager.iCloud() | |
| const dir = fm.documentsDirectory() | |
| const path = fm.joinPath(dir, `icons/${imageName}.png`) | |
| if (fm.fileExists(path)) { | |
| return fm.readImage(path) | |
| } else { | |
| console.log(`${imageName}.png does not exist. Use fileExists(filePath) on a FileManager to check if a file exists.`) | |
| return null | |
| } | |
| } | |
| // Main function to create the widget | |
| async function createWidget() { | |
| const colors = getColors() | |
| const widget = new ListWidget() | |
| widget.backgroundColor = new Color(colors.background) | |
| widget.setPadding(20, 20, 20, 20) | |
| const vault = FileManager.local().bookmarkedPath(vaultName) | |
| const files = await getMarkdownFiles(vault) | |
| let allTasks = [] | |
| for (const file of files) { | |
| const tasks = await parseTasksFromFile(file) | |
| allTasks.push(...tasks) | |
| } | |
| const { assignedToMe, waitingOnOthers } = classifyTasks(allTasks) | |
| const prioritizedAssignedToMe = prioritizeTasks(assignedToMe) | |
| const prioritizedWaitingOnOthers = prioritizeTasks(waitingOnOthers) | |
| const totalAssignedToMe = prioritizedAssignedToMe.length | |
| const totalWaitingOnOthers = prioritizedWaitingOnOthers.length | |
| const myTasksStack = widget.addStack() | |
| myTasksStack.layoutHorizontally() | |
| const myTasksTitle = myTasksStack.addText("My Tasks") | |
| myTasksTitle.font = Font.boldSystemFont(16) | |
| myTasksTitle.textColor = new Color(colors.accent) | |
| myTasksStack.addSpacer() | |
| const myTasksCount = myTasksStack.addText(`(${totalAssignedToMe})`) | |
| myTasksCount.font = Font.mediumSystemFont(14) | |
| myTasksCount.textColor = new Color(colors.accent) | |
| widget.addSpacer(4) | |
| function addTaskToWidget(task, widget, isWaitingOn = false) { | |
| const colors = getColors() | |
| const taskStack = widget.addStack() | |
| taskStack.layoutVertically() | |
| taskStack.spacing = 1 | |
| const taskLine = taskStack.addStack() | |
| taskLine.spacing = 4 | |
| const circle = taskLine.addText("○") | |
| circle.font = Font.mediumSystemFont(12) | |
| circle.textColor = new Color(colors.circle) | |
| let displayText = parseMarkdownLinks(task.text) | |
| displayText = displayText.length > 50 ? displayText.substring(0, 47) + "..." : displayText | |
| const taskText = taskLine.addText(displayText) | |
| taskText.font = Font.regularSystemFont(12) | |
| taskText.textColor = new Color(colors.text) | |
| taskText.lineLimit = 1 | |
| const infoLine = taskStack.addStack() | |
| infoLine.layoutHorizontally() | |
| infoLine.centerAlignContent() // Center align the content vertically | |
| infoLine.spacing = 5 | |
| for (const [dateType, date] of Object.entries(task.dates)) { | |
| const { text, color, status } = formatDueDate(date) | |
| const dateStack = infoLine.addStack() | |
| dateStack.centerAlignContent() // Center align the content vertically | |
| dateStack.spacing = 2 | |
| const icon = getIconImage(dateType, status) | |
| if (icon) { | |
| const iconElement = dateStack.addImage(icon) | |
| iconElement.imageSize = new Size(10, 10) | |
| iconElement.tintColor = color | |
| } else { | |
| const fallbackText = dateStack.addText(dateType.charAt(0)) | |
| fallbackText.font = Font.mediumSystemFont(12) | |
| fallbackText.textColor = color | |
| } | |
| const dateText = dateStack.addText(text) | |
| dateText.font = Font.mediumSystemFont(9) | |
| dateText.textColor = color | |
| infoLine.addSpacer(3) | |
| } | |
| const fileName = getFileName(task.filePath) | |
| if (!fileName.includes('@')) { | |
| infoLine.addSpacer() | |
| const pillStack = infoLine.addStack() | |
| pillStack.centerAlignContent() // Center align the content vertically | |
| pillStack.cornerRadius = 10 | |
| pillStack.setPadding(2, 8, 2, 8) | |
| pillStack.backgroundColor = new Color(colors.pill) | |
| const fileNameText = pillStack.addText(fileName) | |
| fileNameText.font = Font.regularSystemFont(9) | |
| fileNameText.textColor = new Color(colors.pillText) | |
| } | |
| if (isWaitingOn) { | |
| infoLine.addSpacer() | |
| const pillStack = infoLine.addStack() | |
| pillStack.centerAlignContent() | |
| pillStack.cornerRadius = 10 | |
| pillStack.setPadding(2, 8, 2, 8) | |
| pillStack.backgroundColor = new Color(colors.pill) | |
| const firstName = extractFirstName(task) | |
| const nameText = pillStack.addText(firstName) | |
| nameText.font = Font.regularSystemFont(9) | |
| nameText.textColor = new Color(colors.pillText) | |
| } | |
| const encodedFilePath = encodeURIComponent(task.filePath.replace(vault, '')) | |
| taskStack.url = `obsidian://open?vault=${encodeURIComponent(vaultName)}&file=${encodedFilePath}` | |
| } | |
| // Display tasks assigned to me | |
| for (let i = 0; i < Math.min(6, prioritizedAssignedToMe.length); i++) { | |
| addTaskToWidget(prioritizedAssignedToMe[i], widget) | |
| } | |
| widget.addSpacer(4) | |
| const waitingOnStack = widget.addStack() | |
| waitingOnStack.layoutHorizontally() | |
| const waitingOnOthersTitle = waitingOnStack.addText("Waiting On") | |
| waitingOnOthersTitle.font = Font.boldSystemFont(16) | |
| waitingOnOthersTitle.textColor = new Color(colors.accent) | |
| waitingOnStack.addSpacer() | |
| const waitingOnCount = waitingOnStack.addText(`(${totalWaitingOnOthers})`) | |
| waitingOnCount.font = Font.mediumSystemFont(14) | |
| waitingOnCount.textColor = new Color(colors.accent) | |
| widget.addSpacer(4) | |
| // Display tasks waiting on others | |
| for (let i = 0; i < Math.min(3, prioritizedWaitingOnOthers.length); i++) { | |
| addTaskToWidget(prioritizedWaitingOnOthers[i], widget, true) | |
| } | |
| return widget | |
| } | |
| // Helper function to get Unicode icon | |
| function getIconImage(dateType, dueStatus = 'future') { | |
| let imageName | |
| switch (dateType) { | |
| case 'Due': | |
| switch (dueStatus) { | |
| case 'past': imageName = 'flag-red'; break | |
| case 'today': imageName = 'flag-orange'; break | |
| case 'future': imageName = 'flag-green'; break | |
| } | |
| break | |
| case 'Scheduled': imageName = 'calendar'; break | |
| case 'Start': imageName = 'play'; break | |
| case 'End': imageName = 'stop'; break | |
| case 'Repeat': imageName = 'repeat'; break | |
| case 'Date': imageName = 'date'; break | |
| default: imageName = 'pin' | |
| } | |
| const image = loadImage(imageName) | |
| return image || null | |
| } | |
| // Run the script | |
| const widget = await createWidget() | |
| if (config.runsInWidget) { | |
| Script.setWidget(widget) | |
| } else { | |
| widget.presentLarge() | |
| } | |
| Script.complete() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment