Last active
August 1, 2024 14:18
-
-
Save soldni/72c163346e910e33afd730e8cea054a6 to your computer and use it in GitHub Desktop.
A Scriptable widget to display ToDos from a Notion database in a iOS widget.
This file contains hidden or 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: light-gray; icon-glyph: copy; | |
// follow instructions here https://developers.notion.com/docs/create-a-notion-integration | |
// for how to configure an integration, get the bearer token, and authorize the integration | |
// to access a Notion database. | |
const NOTION_DB_LINK = "https://www.notion.so/[YOUR USERNAME]/[LINK TO DATABASE]" | |
const BEARER_TOKEN = "Bearer secret_*******************************************" | |
// The column in the database that contains the task name | |
const TASK_COLUMN_NAME = "Task" | |
// The column in the database that contains a selection of the project name | |
const PROJECT_COLUMN_NAME = "Project" | |
// The column in the database that contains the task status | |
const STATUS_COLUMN_NAME = "Status" | |
// The status values to show in the widget | |
const STATUSES_TO_SHOW = ["Not started", "In progress"] | |
// ------------------------------------------------ | |
// ------------------------------------------------ | |
// WARNING: DO NOT CHANGE ANYTHING BELOW THIS LINE! | |
// ------------------------------------------------ | |
// ------------------------------------------------ | |
var WIDGET_SIZE = ( | |
config.runsInWidget ? | |
config.widgetFamily : | |
"large" | |
); | |
if (args.widgetParameter) { | |
let param = args.widgetParameter.split("|"); | |
if (param.length >= 1) { WIDGET_SIZE = param[0]; } | |
if (param.length >= 2) { SITE_URL = param[1]; } | |
if (param.length >= 3) { SITE_NAME = param[2]; } | |
if (param.length >= 4) { SHOW_POST_IMAGES = param[3]; } | |
if (param.length >= 5) { BG_IMAGE_NAME = param[4]; } | |
if (param.length >= 6) { BG_IMAGE_BLUR = param[5]; } | |
if (param.length >= 7) { BG_IMAGE_GRADIENT = param[6]; } | |
} | |
const NOTION_COLORS = { | |
"default": ["#37352F", "#EEEEEE"], | |
"gray": ["#9B9A97", "#979A9B"], | |
"brown": ["#64473A", "#937264"], | |
"orange": ["#D9730D", "#FFA344"], | |
"yellow": ["#DFAB01", "#FFDC49"], | |
"green": ["#0F7B6C", "#4DAB9A"], | |
"blue": ["#0B6E99", "#529CCA"], | |
"purple": ["#6940A5", "#9A6DD7"], | |
"pink": ["#AD1A72", "#E255A1"], | |
"red": ["#E03E3E", "#FF7369"] | |
} | |
const NOTION_BACKGROUND = { | |
"light": ["#FEFEFE", "#F8F8F8"], | |
"dark": ["#252525", "#121212"] | |
} | |
// set the number of posts depending on WIDGET_SIZE | |
var TASKS_COUNT = 0; | |
var TASKS_COLUMNS = 0; | |
var RELAXED_FACTOR = 0.0; | |
var FONT_FACTOR = 0.0; | |
switch (WIDGET_SIZE) { | |
case "small": | |
TASKS_COUNT = 5; | |
TASKS_COLUMNS = 1; | |
RELAXED_FACTOR = 1.0; | |
FONT_FACTOR = 1.0; | |
break; | |
case "medium": | |
TASKS_COUNT = 5; | |
TASKS_COLUMNS = 2; | |
RELAXED_FACTOR = 1.8; | |
FONT_FACTOR = 1.16; | |
break; | |
case "large": | |
TASKS_COUNT = 10; | |
TASKS_COLUMNS = 2; | |
RELAXED_FACTOR = 3.0; | |
FONT_FACTOR = 1.31; | |
break; | |
} | |
function checkDbFormat(url) { | |
// Use a regular expression to check if the URL is in the correct format | |
const regex = /https:\/\/www\.notion\.so\/([a-zA-Z0-9]+)\/([0-9a-fA-F]+)/; | |
if (!regex.test(url)) { | |
throw new Error("Invalid URL format"); | |
} | |
return url; | |
} | |
function getUserFromUrl(url) { | |
// Validate the URL | |
url = checkDbFormat(url); | |
// Split the URL into an array by '/' character | |
const parts = url.split('/'); | |
// Return the second element of the array | |
// (the username is the second part of the URL) | |
return parts[parts.length - 2]; | |
} | |
function getDbFromUrl(url) { | |
// Validate the URL | |
url = checkDbFormat(url); | |
// Split the URL into an array by '/' character | |
const parts = url.split('/'); | |
// Return the second element of the array | |
// (the database ID is the third part of the URL) | |
return parts[parts.length - 1]; | |
} | |
async function getToDosFromDb(key, url){ | |
// The id of the database to query | |
const db = getDbFromUrl(url); | |
// This is the base URL for the endpoint to query a Notion DB | |
const req_url = `https://api.notion.com/v1/databases/${db}/query`; | |
const filters = []; | |
for (const status of STATUSES_TO_SHOW) { | |
filters.push({ | |
"property": STATUS_COLUMN_NAME, | |
"status": { | |
"equals": status | |
} | |
}); | |
} | |
let req = new Request(req_url); | |
req.method = 'POST'; | |
req.headers = { | |
'Content-Type': 'application/json', | |
'Notion-Version': '2022-06-28', | |
'Authorization': key, | |
}; | |
req.body = JSON.stringify({"filter": {"or": filters}}); | |
let data = await req.loadJSON(); | |
let tasks = []; | |
// loop through the results | |
for (const result of data.results) { | |
// get the title of the task | |
const title = result.properties[TASK_COLUMN_NAME].title[0].plain_text; | |
// get the url of the task | |
const task_url = result.url; | |
// get the status of the task | |
const project = result.properties[PROJECT_COLUMN_NAME].select.name; | |
// get the color of the task | |
const color = result.properties[PROJECT_COLUMN_NAME].select.color; | |
// add the task to the array | |
tasks.push({ | |
title: title, | |
url: task_url, | |
project: project, | |
color: color | |
}); | |
}; | |
// return the tasks | |
return tasks; | |
} | |
async function createWidget() { | |
const listWg = new ListWidget(); | |
listWg.url = 'notion://' + NOTION_DB_LINK; | |
const gradient_top = Color.dynamic( | |
new Color(NOTION_BACKGROUND["light"][0]), | |
new Color(NOTION_BACKGROUND["dark"][0]) | |
) | |
const gradient_bottom = Color.dynamic( | |
new Color(NOTION_BACKGROUND["light"][1]), | |
new Color(NOTION_BACKGROUND["dark"][1]) | |
) | |
const gradient = new LinearGradient(); | |
gradient.locations = [0, 1]; | |
gradient.colors = [gradient_top, gradient_bottom]; | |
listWg.backgroundGradient = gradient; | |
const tasks = await getToDosFromDb( | |
key=BEARER_TOKEN, | |
url=NOTION_DB_LINK | |
); | |
const titleStack = listWg.addStack(); | |
titleStack.layoutVertically(); | |
titleStack.setPadding(1, 1, 4 * RELAXED_FACTOR, 1); | |
const wgTitle = titleStack.addText( | |
`☑️ ${tasks.length} ` + (tasks.length == 1 ? "Task" : "Tasks") | |
); | |
wgTitle.font = Font.boldRoundedSystemFont(15 * FONT_FACTOR); | |
wgTitle.centerAlignText(); | |
const tasksStack = listWg.addStack(); | |
tasksStack.layoutHorizontally(); | |
tasksStack.topAlignContent(); | |
const columnStacks = []; | |
for (var i = 0; i < TASKS_COLUMNS; i++) { | |
var col = tasksStack.addStack(); | |
col.layoutVertically(); | |
col.setPadding(0, 1 * RELAXED_FACTOR, 1, 1 * RELAXED_FACTOR); | |
columnStacks.push(col); | |
} | |
var light_color = new Color(NOTION_COLORS["default"][0]); | |
var dark_color = new Color(NOTION_COLORS["default"][1]); | |
var dynamic_color = Color.dynamic(light_color, dark_color); | |
var row_count = 0; | |
var col_count = 0; | |
var offset_last_column = 0; | |
var written_so_far = 0; | |
// loop through the tasks | |
for (const task of tasks) { | |
row_count += 1; | |
offset_last_column = 1 ? col_count == (TASKS_COLUMNS - 1) : 0; | |
if (row_count > (TASKS_COUNT - offset_last_column)) { | |
col_count += 1; | |
row_count = 1; | |
} | |
if (col_count >= TASKS_COLUMNS) { | |
remaining = tasks.length - written_so_far; | |
var entry = columnStacks[col_count - 1].addText(`+${remaining} more`); | |
light_color = new Color(NOTION_COLORS["default"][0]); | |
dark_color = new Color(NOTION_COLORS["default"][1]); | |
dynamic_color = Color.dynamic(light_color, dark_color); | |
entry.textColor = dynamic_color; | |
entry.font = Font.thinRoundedSystemFont(13); | |
break; | |
} | |
var entry = columnStacks[col_count].addText(task.title); | |
light_color = new Color(NOTION_COLORS[task.color][0]); | |
dark_color = new Color(NOTION_COLORS[task.color][1]); | |
dynamic_color = Color.dynamic(light_color, dark_color); | |
entry.textColor = dynamic_color; | |
entry.font = Font.regularRoundedSystemFont(13 * FONT_FACTOR); | |
columnStacks[col_count].addSpacer(1 * RELAXED_FACTOR); | |
written_so_far += 1; | |
} | |
return listWg; | |
} | |
const widget = await createWidget(); | |
if (!config.runsInWidget) { | |
switch (WIDGET_SIZE) { | |
case "small": | |
await widget.presentSmall(); | |
break; | |
case "medium": | |
await widget.presentMedium(); | |
break; | |
case "large": | |
await widget.presentLarge(); | |
break; | |
} | |
} | |
Script.setWidget(widget); | |
Script.complete(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Could this create a local database for bidirectional database synchronization, with to-do status synchronized with notion