|
// Tasks Widget for Scriptable |
|
// Displays upcoming tasks from your task management system |
|
// Large widget with interactive checkboxes |
|
|
|
// ============ CONFIGURATION ============ |
|
const API_BASE_URL = "YOUR_API_BASE_URL"; // Replace with your actual API base URL |
|
const AUTH_TOKEN = "YOUR_AUTH_TOKEN"; // Replace with your authentication token if needed |
|
const MAX_TASKS_TO_SHOW = 15; // Maximum number of tasks to display (large widget can show more) |
|
const SHOW_ACTIVE_TASKS = true; // Show active tasks (due before now) |
|
const SHOW_UPCOMING_TASKS = true; // Show upcoming tasks (scheduled after now) |
|
|
|
// ============ API FUNCTIONS ============ |
|
|
|
async function fetchTasks(scope) { |
|
const request = new Request(`${API_BASE_URL}/tasks?scope=${scope}`); |
|
if (AUTH_TOKEN) { |
|
request.headers = { "Authorization": `Bearer ${AUTH_TOKEN}` }; |
|
} |
|
const response = await request.loadJSON(); |
|
return response.items || []; |
|
} |
|
|
|
async function getTasks() { |
|
try { |
|
let allTasks = []; |
|
|
|
// Fetch active tasks (tasks due before now that are not completed/cancelled) |
|
if (SHOW_ACTIVE_TASKS) { |
|
const activeTasks = await fetchTasks("active"); |
|
allTasks = allTasks.concat(activeTasks.map(task => ({ |
|
...task, |
|
category: "active" |
|
}))); |
|
} |
|
|
|
// Fetch upcoming tasks (tasks scheduled after now) |
|
if (SHOW_UPCOMING_TASKS) { |
|
const upcomingTasks = await fetchTasks("upcoming"); |
|
allTasks = allTasks.concat(upcomingTasks.map(task => ({ |
|
...task, |
|
category: "upcoming" |
|
}))); |
|
} |
|
|
|
// Filter to only show tasks with dates and not completed |
|
const filteredTasks = allTasks.filter(task => { |
|
const scheduleDate = task.taskInfo?.scheduleDate; |
|
const deadlineDate = task.taskInfo?.deadlineDate; |
|
const hasDate = scheduleDate || deadlineDate; |
|
const isNotDone = task.taskInfo?.state !== "done" && |
|
task.taskInfo?.state !== "canceled" && |
|
task.taskInfo?.state !== "cancelled"; |
|
return hasDate && isNotDone; |
|
}); |
|
|
|
// Sort tasks by date (prefer schedule date, then deadline) |
|
const sortedTasks = filteredTasks.sort((a, b) => { |
|
const dateA = a.taskInfo?.scheduleDate || a.taskInfo?.deadlineDate; |
|
const dateB = b.taskInfo?.scheduleDate || b.taskInfo?.deadlineDate; |
|
|
|
return new Date(dateA) - new Date(dateB); |
|
}); |
|
|
|
return sortedTasks.slice(0, MAX_TASKS_TO_SHOW); |
|
} catch (error) { |
|
console.error("Error fetching tasks:", error); |
|
return []; |
|
} |
|
} |
|
|
|
// ============ WIDGET CREATION ============ |
|
|
|
function cleanMarkdown(markdown) { |
|
if (!markdown) return "Untitled"; |
|
// Remove task checkbox syntax: "- [ ] " or "- [x] " or "- [X] " |
|
return markdown.replace(/^-\s*\[[xX\s]\]\s*/g, "").trim(); |
|
} |
|
|
|
function formatDate(dateString) { |
|
if (!dateString) return ""; |
|
|
|
// Parse as local date to avoid timezone issues |
|
const [year, month, day] = dateString.split('-').map(Number); |
|
const date = new Date(year, month - 1, day); |
|
|
|
const now = new Date(); |
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); |
|
const taskDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); |
|
|
|
const diffDays = Math.floor((taskDate - today) / (1000 * 60 * 60 * 24)); |
|
|
|
if (diffDays === 0) return "Today"; |
|
if (diffDays === 1) return "Tomorrow"; |
|
if (diffDays === -1) return "Yesterday"; |
|
if (diffDays < -1) return `${Math.abs(diffDays)}d overdue`; |
|
if (diffDays <= 7) return `${diffDays}d`; |
|
|
|
const displayMonth = date.getMonth() + 1; |
|
const displayDay = date.getDate(); |
|
return `${displayMonth}/${displayDay}`; |
|
} |
|
|
|
function isOverdue(dateString) { |
|
if (!dateString) return false; |
|
|
|
// Parse as local date to avoid timezone issues |
|
const [year, month, day] = dateString.split('-').map(Number); |
|
const date = new Date(year, month - 1, day); |
|
|
|
const now = new Date(); |
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()); |
|
const taskDate = new Date(date.getFullYear(), date.getMonth(), date.getDate()); |
|
|
|
return taskDate < today; |
|
} |
|
|
|
async function createWidget(tasks) { |
|
const widget = new ListWidget(); |
|
|
|
// Set background gradient |
|
const gradient = new LinearGradient(); |
|
gradient.colors = [new Color("#1c1c1e"), new Color("#2c2c2e")]; |
|
gradient.locations = [0, 1]; |
|
widget.backgroundGradient = gradient; |
|
|
|
widget.setPadding(16, 16, 16, 16); |
|
|
|
// Header |
|
const header = widget.addStack(); |
|
header.layoutHorizontally(); |
|
header.centerAlignContent(); |
|
|
|
const titleText = header.addText("📋 Tasks"); |
|
titleText.font = Font.boldSystemFont(18); |
|
titleText.textColor = Color.white(); |
|
|
|
header.addSpacer(); |
|
|
|
const countText = header.addText(`${tasks.length}`); |
|
countText.font = Font.semiboldSystemFont(16); |
|
countText.textColor = Color.orange(); |
|
|
|
widget.addSpacer(12); |
|
|
|
// Tasks list |
|
if (tasks.length === 0) { |
|
const emptyText = widget.addText("No upcoming tasks! 🎉"); |
|
emptyText.font = Font.systemFont(15); |
|
emptyText.textColor = Color.gray(); |
|
emptyText.centerAlignText(); |
|
} else { |
|
for (let i = 0; i < tasks.length; i++) { |
|
const task = tasks[i]; |
|
|
|
const taskStack = widget.addStack(); |
|
taskStack.layoutHorizontally(); |
|
taskStack.centerAlignContent(); |
|
taskStack.spacing = 10; |
|
|
|
// Static checkbox (non-interactive) |
|
const checkboxStack = taskStack.addStack(); |
|
checkboxStack.size = new Size(20, 20); |
|
checkboxStack.cornerRadius = 4; |
|
checkboxStack.borderWidth = 2; |
|
checkboxStack.borderColor = Color.gray(); |
|
checkboxStack.backgroundColor = new Color("#1c1c1e"); |
|
|
|
// Task content stack |
|
const contentStack = taskStack.addStack(); |
|
contentStack.layoutVertically(); |
|
contentStack.spacing = 2; |
|
|
|
// Task title (cleaned markdown content) |
|
const cleanedMarkdown = cleanMarkdown(task.markdown); |
|
const taskTitle = contentStack.addText(cleanedMarkdown); |
|
taskTitle.font = Font.systemFont(14); |
|
taskTitle.textColor = Color.white(); |
|
taskTitle.lineLimit = 2; |
|
|
|
taskStack.addSpacer(); |
|
|
|
// Date stack for both schedule and deadline (in a row) |
|
const scheduleDate = task.taskInfo?.scheduleDate; |
|
const deadlineDate = task.taskInfo?.deadlineDate; |
|
|
|
if (scheduleDate || deadlineDate) { |
|
const dateStack = taskStack.addStack(); |
|
dateStack.layoutHorizontally(); |
|
dateStack.spacing = 8; |
|
dateStack.centerAlignContent(); |
|
|
|
// Schedule date |
|
if (scheduleDate) { |
|
const scheduleStack = dateStack.addStack(); |
|
scheduleStack.layoutHorizontally(); |
|
scheduleStack.spacing = 3; |
|
scheduleStack.centerAlignContent(); |
|
|
|
const scheduleLabel = scheduleStack.addText("📅"); |
|
scheduleLabel.font = Font.systemFont(10); |
|
|
|
const scheduleDateText = scheduleStack.addText(formatDate(scheduleDate)); |
|
scheduleDateText.font = Font.semiboldSystemFont(12); |
|
|
|
if (isOverdue(scheduleDate)) { |
|
scheduleDateText.textColor = Color.red(); |
|
} else { |
|
// Parse as local date to avoid timezone issues |
|
const [year, month, day] = scheduleDate.split('-').map(Number); |
|
const date = new Date(year, month - 1, day); |
|
|
|
const now = new Date(); |
|
const tomorrow = new Date(now); |
|
tomorrow.setDate(tomorrow.getDate() + 1); |
|
const taskDateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate()); |
|
const tomorrowOnly = new Date(tomorrow.getFullYear(), tomorrow.getMonth(), tomorrow.getDate()); |
|
|
|
if (taskDateOnly <= tomorrowOnly) { |
|
scheduleDateText.textColor = Color.orange(); |
|
} else { |
|
scheduleDateText.textColor = Color.gray(); |
|
} |
|
} |
|
} |
|
|
|
// Deadline date |
|
if (deadlineDate) { |
|
const deadlineStack = dateStack.addStack(); |
|
deadlineStack.layoutHorizontally(); |
|
deadlineStack.spacing = 3; |
|
deadlineStack.centerAlignContent(); |
|
|
|
const deadlineLabel = deadlineStack.addText("‼️"); |
|
deadlineLabel.font = Font.systemFont(10); |
|
|
|
const deadlineDateText = deadlineStack.addText(formatDate(deadlineDate)); |
|
deadlineDateText.font = Font.semiboldSystemFont(12); |
|
|
|
if (isOverdue(deadlineDate)) { |
|
deadlineDateText.textColor = Color.red(); |
|
} else { |
|
// Parse as local date to avoid timezone issues |
|
const [year, month, day] = deadlineDate.split('-').map(Number); |
|
const date = new Date(year, month - 1, day); |
|
|
|
const now = new Date(); |
|
const tomorrow = new Date(now); |
|
tomorrow.setDate(tomorrow.getDate() + 1); |
|
const taskDateOnly = new Date(date.getFullYear(), date.getMonth(), date.getDate()); |
|
const tomorrowOnly = new Date(tomorrow.getFullYear(), tomorrow.getMonth(), tomorrow.getDate()); |
|
|
|
if (taskDateOnly <= tomorrowOnly) { |
|
deadlineDateText.textColor = Color.orange(); |
|
} else { |
|
deadlineDateText.textColor = Color.gray(); |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (i < tasks.length - 1) { |
|
widget.addSpacer(8); |
|
} |
|
} |
|
} |
|
|
|
widget.addSpacer(); |
|
|
|
// Last updated timestamp |
|
const updateTime = new Date(); |
|
const timeFormatter = new DateFormatter(); |
|
timeFormatter.dateFormat = "h:mm a"; |
|
const footer = widget.addText(`Updated ${timeFormatter.string(updateTime)}`); |
|
footer.font = Font.systemFont(10); |
|
footer.textColor = new Color("#666666"); |
|
footer.centerAlignText(); |
|
|
|
return widget; |
|
} |
|
|
|
// ============ MAIN EXECUTION ============ |
|
|
|
async function run() { |
|
try { |
|
const tasks = await getTasks(); |
|
const widget = await createWidget(tasks); |
|
|
|
if (config.runsInWidget) { |
|
Script.setWidget(widget); |
|
} else { |
|
widget.presentLarge(); |
|
} |
|
|
|
Script.complete(); |
|
} catch (error) { |
|
console.error("Widget error:", error); |
|
|
|
// Create error widget |
|
const errorWidget = new ListWidget(); |
|
errorWidget.backgroundColor = new Color("#1c1c1e"); |
|
|
|
const errorText = errorWidget.addText("❌ Error loading tasks"); |
|
errorText.font = Font.boldSystemFont(14); |
|
errorText.textColor = Color.red(); |
|
|
|
errorWidget.addSpacer(4); |
|
|
|
const detailText = errorWidget.addText(error.toString()); |
|
detailText.font = Font.systemFont(10); |
|
detailText.textColor = Color.gray(); |
|
|
|
if (config.runsInWidget) { |
|
Script.setWidget(errorWidget); |
|
} else { |
|
errorWidget.presentLarge(); |
|
} |
|
|
|
Script.complete(); |
|
} |
|
} |
|
|
|
await run(); |