Skip to content

Instantly share code, notes, and snippets.

@bumbu
Created January 21, 2022 04:44
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bumbu/ce867650c7eb35b58c90f6e96dac970e to your computer and use it in GitHub Desktop.
Save bumbu/ce867650c7eb35b58c90f6e96dac970e to your computer and use it in GitHub Desktop.
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: red; icon-glyph: check;
"use strict";
/**
* Widget to get a Todoist task based on a filter
* If a filter has multiple sections, will get a random task from first section that has tasks
* The script remembers last selected task, and keeps showing that task while it matches above
* condition (being part of first section with tasks).
*
* You can find your API_TOKEN from the Todoist Web app, at Todoist Settings -> Integrations -> API token
* For FILTER_NAME you should use exactly the same name as your filter (including casing)
*/
const API_TOKEN = 'REPLACEME'
const FILTER_NAME = 'REPLACEME'
const CACHE_FILE = 'TodoistFocusCache.json'
async function getTask() {
const filters = await getFilters();
const filter = filters.reduce(function(acc, curr){
return curr.name === FILTER_NAME ? curr : acc;
}, null);
if (filter == null) {
console.log(`No filter found by given name`)
console.log(filters);
throw new Error('No filter found by given name');
} else {
console.log(`Filter found ${JSON.stringify(filter)}`)
}
const lastShownTaskId = await getLastShownTaskId();
// Split query by comma, query all in parallel
const queries = filter.query.split(',');
const taskRequests = queries.map(query => getTasksByFilter(query))
const taskSections = await Promise.all(taskRequests)
// Get random task from first section that has a task
for (const tasks of taskSections) {
if (tasks.length > 0) {
let index = Math.floor(Math.random() * tasks.length * 0.999);
// Check if any of the tasks was shown in previous round, then use that one
if (lastShownTaskId != null) {
for (let i = 0; i < tasks.length; i++) {
if (tasks[i].id === lastShownTaskId) {
index = i;
break;
}
}
}
const task = tasks[index]
// Store
await setLastShownTaskId(task.id)
// RETURN here vvv
return task
} else {
// Continue
}
}
return {content: 'NO TASK FOUND'}
}
async function getLastShownTaskId() {
const cache = await getCachedData(CACHE_FILE);
console.log(`cache: ${JSON.stringify(cache)}`)
if (cache.lastTaskId != null) {
return cache.lastTaskId;
} else {
return null;
}
}
async function setLastShownTaskId(id) {
const cache = await getCachedData(CACHE_FILE);
cache.lastTaskId = id;
await cacheData(CACHE_FILE, cache);
}
/**
* Get JSON from a local file
*
* @param {string} fileName
* @returns {object}
*/
function getCachedData(fileName) {
const fileManager = FileManager.iCloud();
const cacheDirectory = fileManager.joinPath(fileManager.libraryDirectory(), "cache");
const cacheFile = fileManager.joinPath(cacheDirectory, fileName);
if (!fileManager.fileExists(cacheFile)) {
return {};
}
const contents = fileManager.readString(cacheFile);
return JSON.parse(contents);
}
/**
* Wite JSON to a local file
*
* @param {string} fileName
* @param {object} data
*/
function cacheData(fileName, data) {
const fileManager = FileManager.iCloud();
const cacheDirectory = fileManager.joinPath(fileManager.libraryDirectory(), "cache");
const cacheFile = fileManager.joinPath(cacheDirectory, fileName);
if (!fileManager.fileExists(cacheDirectory)) {
fileManager.createDirectory(cacheDirectory);
}
const contents = JSON.stringify(data);
fileManager.writeString(cacheFile, contents);
}
async function getFilters() {
const req = new Request(
`https://api.todoist.com/sync/v8/sync`
);
req.method = 'POST';
req.headers = {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_TOKEN}`,
};
req.body = JSON.stringify({
resource_types: '["filters"]',
});
const res = await req.loadJSON();
return res.filters;
}
async function getTasksByFilter(filter) {
const urlParams = '?filter=' + encodeURIComponent(filter)
const req = new Request(
`https://api.todoist.com/rest/v1/tasks${urlParams}`
);
req.method = 'GET';
req.headers = {
"Content-Type": "application/json",
"Authorization": `Bearer ${API_TOKEN}`,
};
const res = await req.loadJSON();
return res;
}
async function run() {
const listWidget = new ListWidget();
listWidget.useDefaultPadding();
try {
const task = await getTask();
const startColor = Color.dynamic(Color.gray(), Color.gray());
const endColor = Color.dynamic(Color.lightGray(), Color.lightGray());
const textColor = Color.dynamic(Color.black(), Color.black());
// BACKGROUND
const gradient = new LinearGradient();
gradient.colors = [startColor, endColor];
gradient.locations = [0.0, 1];
console.log({ gradient });
listWidget.backgroundGradient = gradient;
// MAIN Stack
const widgetStack = listWidget.addStack();
widgetStack.layoutHorizontally();
widgetStack.topAlignContent();
widgetStack.setPadding (0,0,0,0);
const contentStack = widgetStack.addStack();
contentStack.layoutVertically();
contentStack.topAlignContent();
contentStack.setPadding (0,0,0,0);
// Helps with keeping contentStack to the left
widgetStack.addSpacer();
// HEADER
const headStack = contentStack.addStack();
headStack.layoutHorizontally();
headStack.topAlignContent();
headStack.setPadding (0,0,0,0);
const header = headStack.addText('Top Task'.toUpperCase());
header.textColor = textColor;
header.font = Font.regularSystemFont(11);
header.minimumScaleFactor = 1;
// TASK
const taskStack = contentStack.addStack();
taskStack.layoutHorizontally();
taskStack.topAlignContent();
taskStack.setPadding (0,0,0,0);
const taskTitle = taskStack.addText(task.content);
taskTitle.textColor = textColor;
taskTitle.font = Font.semiboldSystemFont(25);
taskTitle.minimumScaleFactor = 0.3;
// CONTENT FOOTER
// Helps with keeping content aligned to top
contentStack.addSpacer();
// TAP HANDLER
listWidget.url = `todoist://task?id=${task.id}`;
} catch (error) {
if (error === 666) {
// Handle JSON parsing errors with a custom error layout
listWidget.background = new Color('999999');
const header = listWidget.addText('Error'.toUpperCase());
header.textColor = new Color('000000');
header.font = Font.regularSystemFont(11);
header.minimumScaleFactor = 0.50;
listWidget.addSpacer(15);
const wordLevel = listWidget.addText(`Couldn't connect to the server.`);
wordLevel.textColor = new Color ('000000');
wordLevel.font = Font.semiboldSystemFont(15);
wordLevel.minimumScaleFactor = 0.3;
} else {
console.log(`Could not render widget: ${error}`);
const errorWidgetText = listWidget.addText(`${error}`);
errorWidgetText.textColor = Color.red();
errorWidgetText.textOpacity = 30;
errorWidgetText.font = Font.regularSystemFont(10);
}
}
if (config.runsInApp) {
listWidget.presentSmall();
}
Script.setWidget(listWidget);
Script.complete();
}
await run();
@joostdewaal
Copy link

This looks terrific. I've literally been looking for this for years. I even considered hiring someone to make it. (I dabbled in code once, but I'm not a programmer by any stretch.)
Do you think there's any option of porting this to a Android widget?

@bumbu
Copy link
Author

bumbu commented Aug 15, 2023

Thanks @joostdewaal for kind words. Unfortunately I don't have Android, and it doesn't look like Scriptable app (that this script is for) is available for Android. You may try your luck on reddit todoist.

p.s. for people who get to this gist, I put up a blog post showing how this widget looks like https://bumbu.me/todoist-scriptable-widget/

@joostdewaal
Copy link

Would you mind if I tried to build an Android version based on your code with some help from friends?

@bumbu
Copy link
Author

bumbu commented Aug 15, 2023

@joostdewaal I'd be happy if it can help you. Feel free to use it in any way you wish. If you get a working version - leave a comment and I'll happily add a link to the blog post as well.

@joostdewaal
Copy link

OK, thanks, we'll give it a shot. **

Copy link

ghost commented Aug 17, 2023

Would you mind if I tried to build an Android version based on your code with some help from friends?

Hi , What exactly do you want in Android?
could you please clarify more?
by the way I don't have IOS 📱

Copy link

ghost commented Aug 17, 2023

@joostdewaal I'd be happy if it can help you. Feel free to use it in any way you wish. If you get a working version - leave a comment and I'll happily add a link to the blog post as well.

Hi Bumbu ,I would love to see more of your creative program👌,
do you have a video file of your program?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment