Skip to content

Instantly share code, notes, and snippets.

@dillondcarter
Forked from alexberkowitz/Todoist Widget 1.js
Created August 6, 2022 17:17
Show Gist options
  • Save dillondcarter/8bd2234b0c3b7e5321c32a7d284188ed to your computer and use it in GitHub Desktop.
Save dillondcarter/8bd2234b0c3b7e5321c32a7d284188ed to your computer and use it in GitHub Desktop.
Todoist Widget for Scriptable
/******************************************************************************
* Info
*****************************************************************************/
// This script allows you to display your Todoist tasks as an iOS & MacOS
// widget using the app Scriptable.
//
// You can specify a filter to only display certain tasks.
//
// The script will display the priority of the tasks next to their
// content as colored dots. Tasks that are given a label of "Blocked" will
// be displayed as dimmed (check the configuration below to enable/disable features).
/******************************************************************************
* Constants and Configurations
*****************************************************************************/
// NOTE: This script uses the Cache script (https://github.com/kylereddoch/scriptable/src/Cache.js)
// Make sure to add the Cache script in Scriptable as well!
// Cache keys and default location
// If you have multiple instances of the script, make sure these values are unique for each one.
const CACHE_KEY_LAST_UPDATED = 'last_updated';
const CACHE_TODOS = 'todoist_todos';
// Get current date and time
const updatedAt = new Date().toLocaleString();
// Layout and style configuration
const FONT_SIZE = 12;
const COLORS = {
bg: '#1f1f1f',
title_bg: '#2c2c2c',
p1: '#ff7066',
p2: '#ff9a14',
p3: '#5297ff',
p4: '#444444'
};
const PADDING = 15;
const LINE_SPACING = 4.85;
const TITLE = 'Things to do today:';
const EMPTY_MESSAGE = 'You\'re all done for today! 👍';
// TODO: SET THESE VALUES
const API_TOKEN = 'YOUR_API_TOKEN_HERE';
// Filter must be converted into a URL string
const FILTER = "";
// Max number of lines
const MAX_LINES = 6;
// Max number of columns
const MAX_COLUMNS = 2;
// Whether or not to show the "+" to add a task
const SHOW_ADD_BUTTON = true;
// Whether or not to show colored priority dots next to todos
const SHOW_PRIORITY_COLORS = true;
// Whether or not to dim todos that have the "Blocked" label
const DIM_BLOCKED_TODOS = true;
/******************************************************************************
* Initial Setups
*****************************************************************************/
// Import and setup Cache
const Cache = importModule('Cache');
const cache = new Cache('TodoistItems');
// Fetch data and create widget
const data = await fetchData();
const labels = await fetchLabels();
const widget = createWidget(data);
Script.setWidget(widget);
Script.complete();
/******************************************************************************
* Main Functions (Widget and Data-Fetching)
*****************************************************************************/
/**
* Main widget function.
*
* @param {} data The data for the widget to display
*/
function createWidget(data) {
//-- Initialize the widget --\\
const widget = new ListWidget();
widget.backgroundColor = new Color(COLORS.bg);;
widget.setPadding(0, 0, 0, 0);
// Specifying the refreshAfterDate improves refresh times
let nextRefresh = Date.now() + 1000*30;
widget.refreshAfterDate = new Date(nextRefresh);
//-- Main Content Container --\\
const contentStack = widget.addStack();
contentStack.layoutVertically();
contentStack.spacing = LINE_SPACING/2;
//-- Title Bar --\\
const titleStack = contentStack.addStack();
titleStack.backgroundColor = new Color(COLORS.title_bg);
const titleVertOffset = 1.5; // Helps to optically center the title in its container
// Title text
const titleTextStack = titleStack.addStack();
titleTextStack.setPadding(PADDING/2+titleVertOffset, PADDING, PADDING/2-titleVertOffset, PADDING);
const titleText = titleTextStack.addText(TITLE);
titleText.textColor = Color.white();
titleText.textOpacity = 0.75;
titleText.font = Font.boldSystemFont(FONT_SIZE+2); // Title is slightly larger than tasks
titleStack.addSpacer();
// Add task button
if( SHOW_ADD_BUTTON ){
const titleButtonContainerStack = titleStack.addStack();
titleButtonContainerStack.setPadding(PADDING/2+titleVertOffset, PADDING, PADDING/2-titleVertOffset, PADDING);
const addButton = titleButtonContainerStack.addText("+");
addButton.textColor = Color.white();
addButton.font = Font.boldSystemFont(FONT_SIZE+2);
addButton.url = "todoist://addtask";
}
//-- Todo Items --\\
let todos = data.cachedTodos || []; // Get todo list
if( todos.length ){ // If there are items in the list
// Item container
const todoContainer = contentStack.addStack();
todoContainer.layoutHorizontally();
todoContainer.setPadding(PADDING/2, PADDING, PADDING/2, PADDING);
todoContainer.spacing = 15; // Column spacing
// Item list
todos.sort((a, b) => (a.content < b.content) ? 1 : -1); // Sort todos alphabetically
todos.sort((a, b) => (a.priority < b.priority) ? 1 : -1); // Sort todos by priority
let columns = Math.min(Math.ceil(todos.length / MAX_LINES), MAX_COLUMNS); // Number of columns to create
let columnObj = []; // Array to store columns
for( let i = 0; i < columns; i++ ){
columnObj[i] = todoContainer.addStack();
columnObj[i].layoutVertically();
columnObj[i].spacing = LINE_SPACING;
let sliceStart = MAX_LINES * i;
let sliceEnd = MAX_LINES * (i+1);
for(const item of todos.slice(sliceStart, sliceEnd)){
addTodo(columnObj[i], item);
}
}
} else { // Empty state when there are no items
contentStack.addSpacer();
const emptyMessageStack = contentStack.addStack();
emptyMessageStack.addSpacer();
const emptyMessage = emptyMessageStack.addText(EMPTY_MESSAGE);
emptyMessage.textColor = new Color("#666666");
emptyMessage.font = Font.boldSystemFont(FONT_SIZE+2);
emptyMessage.centerAlignText();
emptyMessageStack.addSpacer();
contentStack.addSpacer();
}
widget.addSpacer(); // Push the content up
return widget;
}
/*
* Fetch pieces of data for the widget.
*/
async function fetchData() {
// Get the todo data
const todos = await fetchToDos();
cache.write(CACHE_TODOS, todos);
// Get last data update time (and set)
const lastUpdated = await getLastUpdated();
cache.write(CACHE_KEY_LAST_UPDATED, new Date().getTime());
// Read todos from the cache
let cachedTodos = await cache.read(CACHE_TODOS);
return {
cachedTodos,
lastUpdated,
};
}
/******************************************************************************
* Helper Functions
*****************************************************************************/
//-------------------------------------
// Todoist Helper Functions
//-------------------------------------
/*
* Fetch the todo items from Todoist
*/
async function fetchToDos() {
const url = "https://api.todoist.com/rest/v1/tasks?filter=" + FILTER;
const headers = {
"Authorization": "Bearer " + API_TOKEN
};
const data = await fetchJson(url, headers);
if (!data) {
return 'No data found';
}
// Preview the data response for testing purposes
// let str = JSON.stringify(data, null, 2);
// await QuickLook.present(str);
return data;
}
/*
* Fetch all labels from Todoist
*/
async function fetchLabels() {
const url = "https://api.todoist.com/rest/v1/labels/";
const headers = {
"Authorization": "Bearer " + API_TOKEN
};
const data = await fetchJson(url, headers);
if (!data) {
return 'No data found';
}
// Preview the data response for testing purposes
// let str = JSON.stringify(data, null, 2);
// await QuickLook.present(str);
return data;
}
/*
* Add an item to the given stack
*/
function addTodo(stack, item){
// Create item stack
let todoItem = stack.addStack();
// Add bullet, colored to match priority
if( SHOW_PRIORITY_COLORS ){
let todoBullet = todoItem.addText(String("● "));
todoBullet.font = Font.systemFont(FONT_SIZE);
switch( item.priority ){
case 4:
todoBullet.textColor = new Color(COLORS.p1);
break;
case 3:
todoBullet.textColor = new Color(COLORS.p2)
break;
case 2:
todoBullet.textColor = new Color(COLORS.p3)
break;
default:
todoBullet.textColor = new Color(COLORS.p4);
break;
}
}
// Add item content
let todoContent = todoItem.addText(item.content);
todoContent.textColor = Color.white();
todoContent.font = Font.systemFont(FONT_SIZE);
todoContent.lineLimit = 1;
// If the item is blocked, give it a unique style
if( DIM_BLOCKED_TODOS && isLabelPresent(item.label_ids, "Blocked") ){
todoContent.textOpacity = 0.5;
}
}
/*
* Search for a given label in the master labels list
*/
function isLabelPresent(labelList, labelName){
let labelIsPresent = false;
// For each label in the list, first find the matching entry in
// the master labels list. Then, check if its name matches the
// labelName passed into the function.
labelList.forEach(function(labelID){
let labelInstance = labels.filter(obj => {
return obj.id === labelID;
});
if( labelInstance[0].name == labelName ){
labelIsPresent = true;
}
});
return labelIsPresent;
}
//-------------------------------------
// Misc. Helper Functions
//-------------------------------------
/**
* Make a REST request and return the response
*
* @param {*} url URL to make the request to
* @param {*} headers Headers for the request
*/
async function fetchJson(url, headers) {
try {
console.log(`Fetching url: ${url}`);
const req = new Request(url);
req.method = "get";
req.headers = headers;
const resp = await req.loadJSON();
return resp;
} catch (error) {
console.error(error);
}
}
/*
* Get the last updated timestamp from the Cache.
*/
async function getLastUpdated() {
let cachedLastUpdated = await cache.read(CACHE_KEY_LAST_UPDATED);
if (!cachedLastUpdated) {
cachedLastUpdated = new Date().getTime();
cache.write(CACHE_KEY_LAST_UPDATED, cachedLastUpdated);
}
return cachedLastUpdated;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment