Skip to content

Instantly share code, notes, and snippets.

@soldni
Last active January 8, 2024 01:45
Show Gist options
  • Save soldni/72c163346e910e33afd730e8cea054a6 to your computer and use it in GitHub Desktop.
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.
// 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();
@saibiaochen
Copy link

Could this create a local database for bidirectional database synchronization, with to-do status synchronized with notion

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