Skip to content

Instantly share code, notes, and snippets.

@bumbu
Created January 21, 2022 04:44
Show Gist options
  • 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();
@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