Created
January 21, 2022 04:44
-
-
Save bumbu/ce867650c7eb35b58c90f6e96dac970e to your computer and use it in GitHub Desktop.
This file contains 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: 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(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Bumbu ,I would love to see more of your creative program👌,
do you have a video file of your program?