diff --git a/ios_widget/Notion Block View.js b/ios_widget/Notion Block View.js deleted file mode 100644 index 6bf718f..0000000 --- a/ios_widget/Notion Block View.js +++ /dev/null @@ -1,183 +0,0 @@ -// Variables used by Scriptable. -// These must be at the very top of the file. Do not edit. -// icon-color: deep-blue; icon-glyph: book; - -const NOTION_TOKEN = "" - -const timestamp = new Date().toLocaleTimeString() - -// Determine the block URL (from various sources) -const blockUrl = args.shortcutParameter || args.widgetParameter - -// Early exit, which triggers when used in the 'refresh' iOS Shortcut -// For some reason, this lets the widgets 'rerun' properly and update on command -if (!blockUrl) { - Script.complete() - return -} - -// Determine block type and extract the correct ids from block URL -const collectionViewRegex = /\/(?\w+)\?v=(?\w+$)/ -const blockRegex = /\/.*?(?\w+)$/ -const collectionViewMatches = blockUrl.match(collectionViewRegex) -const blockMatches = blockUrl.match(blockRegex) -let blockType, collectionId, viewId, blockId -if (collectionViewMatches) { - collectionId = collectionViewMatches.groups.collectionId - viewId = collectionViewMatches.groups.viewId - blockType = "collectionView" -} else { - blockId = blockMatches.groups.blockId - blockType = "block" -} - -let widget = new ListWidget() -if (blockType === "block") { - await applyBlockStyle(widget) -} else { - await applyCollectionStyle(widget) -} - -if (config.runsInWidget) { - Script.setWidget(widget) -} else { - widget.presentLarge() -} - -Script.complete() - -async function applyBlockStyle(widget) { - const blockJson = await fetchBlock(blockId) - - addTitle(widget, blockJson["parent"]["title"], `notion://www.notion.so/${blockId}`) - - const childrenStack = widget.addStack() - childrenStack.layoutVertically() - blockJson["children"].forEach(childJson => { - if (!("title" in childJson)) { return } - addChildRow(childrenStack, childJson, "title") - }); - - // Push content to the top - widget.addSpacer(null) - - if (!config.runsWithSiri) { - addFooter(widget, `blocks/${blockId}/children`) - } - - return widget -} - -async function applyCollectionStyle() { - const collectionViewJson = await fetchCollectionView(collectionId, viewId) - - addTitle(widget, collectionViewJson["collection"]["collection_title"], `notion://www.notion.so/${collectionId}?v=${viewId}`) - - const childrenStack = widget.addStack() - childrenStack.layoutVertically() - collectionViewJson["rows"].forEach(childJson => { - if (!("name" in childJson)) { return } - addChildRow(childrenStack, childJson, "name") - }); - - // Push content to the top - widget.addSpacer(null) - - if (!config.runsWithSiri) { - addFooter(widget, `collections/${collectionId}/${viewId}`) - } - - return widget -} - -function addChildRow(childrenStack, childJson, textKey) { - if (!childJson[textKey]) { return } - - childStack = childrenStack.addStack() - childStack.centerAlignContent() - - const deleteSymbol = SFSymbol.named("trash.fill") - const deleteElement = childStack.addImage(deleteSymbol.image) - deleteElement.imageSize = new Size(16, 16) - deleteElement.tintColor = Color.white() - deleteElement.imageOpacity = 0.75 - deleteElement.url = `shortcuts://run-shortcut?name=Delete%20Notion%20Block&input=${childJson["id"]}` - - childStack.addSpacer(8) - - const titleElement = childStack.addText(childJson[textKey]) - titleElement.textColor = Color.white() - titleElement.font = Font.mediumSystemFont(16) - titleElement.minimumScaleFactor = 0.75 - titleElement.url = `notion://www.notion.so/${childJson["id"]}` - - childrenStack.addSpacer(8) -} - -function addTitle(widget, title, url) { - const titleStack = widget.addStack() - titleStack.url = url - titleStack.centerAlignContent() - - const linkSymbol = SFSymbol.named("arrow.up.right.square.fill") - const linkElement = titleStack.addImage(linkSymbol.image) - linkElement.imageSize = new Size(16, 16) - linkElement.tintColor = Color.white() - - titleStack.addSpacer(6) - - const titleElement = titleStack.addText(title) - titleElement.textColor = Color.white() - titleElement.font = Font.boldSystemFont(18) - titleElement.minimumScaleFactor = 0.75 - widget.addSpacer(6) -} - -function addFooter(widget, addUrlPath) { - const footerStack = widget.addStack() - footerStack.bottomAlignContent() - - const addSymbol = SFSymbol.named("plus.square.fill") - const addElement = footerStack.addImage(addSymbol.image) - addElement.imageSize = new Size(20, 20) - addElement.tintColor = Color.white() - addElement.url = `shortcuts://run-shortcut?name=Append%20to%20Notion%20Block&input=${addUrlPath}` - - footerStack.addSpacer(null) - - const refreshStack = footerStack.addStack() - refreshStack.url = `shortcuts://run-shortcut?name=Refresh%20Notion%20Block&input=${blockUrl}` - refreshStack.centerAlignContent() - - const refreshSymbol = SFSymbol.named("arrow.clockwise.icloud.fill") - const refreshElement = refreshStack.addImage(refreshSymbol.image) - refreshElement.imageSize = new Size(20, 20) - refreshElement.tintColor = Color.white() - - refreshStack.addSpacer(5) - - const updatedAtElement = refreshStack.addText(`Last Sync: ${timestamp}`) - updatedAtElement.textColor = Color.white() - updatedAtElement.textOpacity = 0.6 - updatedAtElement.font = Font.mediumSystemFont(10) -} - -async function fetchBlock(blockId) { - const request = notionServerRequest(`https://notion-server.herokuapp.com/blocks/${blockId}/children`) - return await request.loadJSON() -} - -async function fetchCollectionView(collectionId, viewId) { - const request = notionServerRequest(`https://notion-server.herokuapp.com/collections/${collectionId}/${viewId}`) - return await request.loadJSON() -} - -function notionServerRequest(url) { - const request = new Request(url) - request.method = 'GET' - request.headers = { - 'Notion-Token': NOTION_TOKEN - } - - return request -} diff --git a/ios_widget/README.md b/ios_widget/README.md index bfc8b77..7fce5cd 100644 --- a/ios_widget/README.md +++ b/ios_widget/README.md @@ -1,47 +1,48 @@ # Scriptable -- Notion Block View -This is a [Scriptable](https://scriptable.app/) widget that provides basic functionality for viewing/creating/deleting Notion blocks using iOS Shortcuts. - - - -# Demo - -I announced and released this on [my blog in this article](https://kevinjalbert.com/custom-notion-ios-widget/). A full demo of the widget is presented in the following YouTube video. - -
- - - -
- -## Installation - -To use this widget you have to do the following: - -1. Acquire your `token_v2` from [Notion's web application](https://www.notion.so/). - -2. (Optional) have your own [notion-toolbox server](../server) running (if the security concern of sending your token to my server scares you) - -3. Install [Scriptable](https://scriptable.app/) on your iOS device - -4. Install [Data Jar](https://datajar.app/) on iOS your iOS device (this is optional if you want to hardcode the notion token in the iOS Shortcuts) and put the `token_v2` value under a new `notion_token` text key. - -5. Create a new script (`Notion Block View`) in Scriptable with the contents in this [file](./Notion%20Block%20View.js) and replace the `NOTION_TOKEN` with your `token_v2` value (and maybe the server url if you decided to use your own) - -6. Add a Scriptable widget on your homescreen in iOS - - 6a. Configure the widget's _Script_ to be `Notion Block View` - - 6b. Configure the widget's _When Interacting_ to be `Run Script` - - 6c. Configure the widget's _Parameter_ to be a Notion link for a page/collection - -7. Create the following iOS Shortcuts on your device (names are important and case-sensitive), you might have to change some things based on the server URL and usage of Data Jar: - - 7a. [Append to Notion Block (image)](./images/shortcut-append.jpeg) - - 7b. [Delete Notion Block (image)](./images/shortcut-delete.jpeg) - - 7c. [Refresh Notion Block (image)](./images/shortcut-refresh.jpeg) - -8. Enjoy +This is a [Scriptable](https://scriptable.app/) widget that provides basic functionality for viewing/creating/deleting Notion blocks. + + + +# Make README md 1611a8ff3dd64c2aae3124a5822142a2 + +# Description + +This repository contains code and short instructions for creating a widget filled with data from your database in Notion. + +«Scriptable» automatically updates the information inside the widget every 5-7 minutes. + +## Brief description of the widget features: + +1. Clicking on a widget - when clicked launches a main script branch that launches the Notion app +2. Text strings - contain text from one of the fields in your Notion database. When you click on the text, a branch of the main script is called, which opens the corresponding page in Notion +3. Circle before each entry - calls a script that transfers the task to the final status (for example: Done) +4. Square with a handle - The button calls Alert, with which you can create a task in the initial status. +5. Round Arrow - Forces a widget refresh. + +# Installation + +1. Create a new database or use [my template](https://www.notion.so/77bcd7e231d84566a2959e6abea33c2d). + 1. It should be understood that the database may have: + 1. Other structure + 2. Other field names + 3. Status names + 4. And so on. + 2. To do this, you will need to change the corresponding lines in the script code. +2. Follow the steps from [this instruction](https://developers.notion.com/docs) + 1. At this step, it is important to save + 1. Your API Token + 2. Your Database ID +3. [Install Scriptable](https://apps.apple.com/ru/app/scriptable/id1405459188) from the App Store +4. Create 3 scripts in Scriptable (names are important and case-sensitive) + 1. [to-do](https://github.com/homerostov/notion-toolbox/blob/master/ios_widget/to_do.js) + 2. [task_done](https://github.com/homerostov/notion-toolbox/blob/master/ios_widget/task_done.js) + 3. [new_task](https://github.com/homerostov/notion-toolbox/blob/master/ios_widget/new_task.js) +5. Go to [to_do](https://github.com/homerostov/notion-toolbox/blob/master/ios_widget/to_do.js#:~:text=let%20notion_token%20%3D-,%3CYour_notion_token%3E,-%3B) script and paste your Token Notion API +6. Add widget + 1. Call the menu for changing the widget and specify the following parameters + 1. Script: to_do + 2. When interacting: Run Script + 3. Parameter: [Your Database ID] + 2. Click Done +7. After a few seconds, the widget will update and display pages from your Notion database diff --git a/ios_widget/images/shortcut-append.jpeg b/ios_widget/images/shortcut-append.jpeg deleted file mode 100644 index d58064a..0000000 Binary files a/ios_widget/images/shortcut-append.jpeg and /dev/null differ diff --git a/ios_widget/images/shortcut-delete.jpeg b/ios_widget/images/shortcut-delete.jpeg deleted file mode 100644 index c4b7208..0000000 Binary files a/ios_widget/images/shortcut-delete.jpeg and /dev/null differ diff --git a/ios_widget/images/shortcut-refresh.jpeg b/ios_widget/images/shortcut-refresh.jpeg deleted file mode 100644 index 9b8e052..0000000 Binary files a/ios_widget/images/shortcut-refresh.jpeg and /dev/null differ diff --git a/ios_widget/images/widget.jpeg b/ios_widget/images/widget.jpeg deleted file mode 100644 index 8b17bd4..0000000 Binary files a/ios_widget/images/widget.jpeg and /dev/null differ diff --git a/ios_widget/images/widget.jpg b/ios_widget/images/widget.jpg new file mode 100644 index 0000000..e232d83 Binary files /dev/null and b/ios_widget/images/widget.jpg differ diff --git a/ios_widget/new_task.js b/ios_widget/new_task.js new file mode 100644 index 0000000..9e38b0f --- /dev/null +++ b/ios_widget/new_task.js @@ -0,0 +1,58 @@ +let alert = new Alert() +alert.title = "New Task" +alert.addTextField(); +alert.addAction("Add Task"); +alert.addCancelAction("Cancel") +await alert.presentAlert(); +let task = alert.textFieldValue(0) +if (task){ + + let req = new Request("https://api.notion.com/v1/pages"); + req.method = "post"; + req.headers = { + 'Authorization': 'Bearer '+ args.queryParameters.token, + 'Notion-Version': '2021-08-16', + "Content-Type": "application/json" + }; + + req.body = JSON.stringify({ + "parent": { + "database_id": args.queryParameters.parent_id + }, + "properties": { + "State": { + "type": "select", + "select": { + "name": "Open" + } + }, + "Name": { + "title": [ + { + "text": { + "content": task + } + } + ] + } + } + }); + + const res = await req.loadJSON() + + let refresh = new CallbackURL("scriptable:///run/") + refresh.addParameter("scriptName", "to_do") + refresh.addParameter("limit" , args.queryParameters.limit.toString()) + refresh.addParameter("parent_id", args.queryParameters.parent_id.toString()) + refresh.open() + +} +else { + App.close() +} +function get_week(date){ + var oneJan = new Date(date.getFullYear(),0,1); + var numberOfDays = Math.floor((date - oneJan) / (24 * 60 * 60 * 1000)); + var resultq = Math.ceil(( date.getDay() + 1 + numberOfDays) / 7); +return resultq; +} diff --git a/ios_widget/task_done.js b/ios_widget/task_done.js new file mode 100644 index 0000000..da21809 --- /dev/null +++ b/ios_widget/task_done.js @@ -0,0 +1,26 @@ +let req = new Request("https://api.notion.com/v1/pages/" + args.queryParameters.id); +req.method = "PATCH"; +req.headers = { + 'Authorization': 'Bearer ' + args.queryParameters.token, + 'Notion-Version': '2021-08-16', + "Content-Type": "application/json" +}; + +req.body = JSON.stringify({ + "properties":{ + "State":{ + "select":{ + "name":"Done" + } + } + } +}); + +const res = await req.loadJSON() + + +let refresh = new CallbackURL("scriptable:///run/") +refresh.addParameter("scriptName", "to_do") +refresh.addParameter("limit" , args.queryParameters.limit.toString()) +refresh.addParameter("parent_id", args.queryParameters.parent_id.toString()) +refresh.open() diff --git a/ios_widget/to_do.js b/ios_widget/to_do.js new file mode 100644 index 0000000..18cd279 --- /dev/null +++ b/ios_widget/to_do.js @@ -0,0 +1,130 @@ +let notion_token = ; + +let notion_db_id = args.widgetParameter || args.queryParameters.parent_id +let req = new Request("https://api.notion.com/v1/databases/" + notion_db_id + "/query"); +req.method = "POST"; +req.headers = { + 'Authorization': 'Bearer ' + notion_token, + 'Notion-Version': '2021-08-16', + 'Content-Type': 'application/json' +}; + +req.body = JSON.stringify({ + "filter": { + "or": [ + { + "property": "State", + "select": { + "equals": "In Progress" + } + }, + { + "property": "State", + "select": { + "equals": "Open" + } + } + ] + }, + "sorts": [ + { + "property": "Last edited time", + "direction": "ascending" + } + ] + }); + +let res_body = await req.loadJSON() +App.close() + +let widget = createWidget(res_body.results) +Script.setWidget(widget) +Script.complete() + + +function createWidget(res, widget_color, elements_color) { + if (config.widgetFamily){ + switch(config.widgetFamily){ + case "small": + limit = 3 + break; + case "medium": + limit = 5 + break; + case "large": + limit = 9 + break; + } + } else { + limit = URLScheme.parameter("limit") + } + + widget_color = "#202125" + elements_color = Color.white() + + let w = new ListWidget() + w.backgroundColor = new Color(widget_color) + w.addSpacer() + + for (let step = 0; step < res.length ; step++) { + if (step == limit){ + break; + } + + const doneTaskSymbol = SFSymbol.named("circle") + let taskStack = w.addStack() + const doneTask = taskStack.addImage(doneTaskSymbol.image) + doneTask.imageSize = new Size(20, 20) + doneTask.imageOpacity = 0.5 + doneTask.tintColor = elements_color + doneTaskcontainerRelativeShape = true + + let cb_done_task = new CallbackURL("scriptable:///run/") + cb_done_task.addParameter("scriptName", "task_done") + cb_done_task.addParameter("token", notion_token) + cb_done_task.addParameter("parent_id", notion_db_id.toString()) + cb_done_task.addParameter("limit", limit.toString()) + cb_done_task.addParameter("id", res[step].id.toString()) + doneTask.url = cb_done_task.getURL() + + taskStack.addSpacer(3) + let taskName = taskStack.addText(res[step].properties.Name.title[0].text.content) + taskName.textColor= elements_color + taskName.textOpacity = 0.8 + taskName.font = Font.mediumSystemFont(16) + taskStack.url = 'notion://' + res[step].url.replace(/(^\w+:|^)\/\//, '') + + if (step < limit - 1) { + w.addSpacer() + } + } + + w.url = 'notion://' + "www.notion.so/" + notion_db_id + + const footerStack = w.addStack() + + + const addTaskSymbol = SFSymbol.named("square.and.pencil") + const addTask = footerStack.addImage(addTaskSymbol.image) + addTask.imageSize = new Size(25, 25) + addTask.tintColor = elements_color + addTask.imageOpacity = 0.5 + let cb_addTask = new CallbackURL("scriptable:///run/") + cb_addTask.addParameter("scriptName", "new_task") + cb_addTask.addParameter("token", notion_token) + cb_addTask.addParameter("parent_id", notion_db_id.toString()) + cb_addTask.addParameter("limit", limit.toString()) + addTask.url = cb_addTask.getURL() + + footerStack.addSpacer() + + const refreshStack = footerStack.addStack() + refreshStack.url = "scriptable:///run?scriptName=to_do&limit=" + limit + "&parent_id=" + notion_db_id + const refreshSymbol = SFSymbol.named("goforward") + const refreshElement = refreshStack.addImage(refreshSymbol.image) + refreshElement.imageSize = new Size(25, 25) + refreshElement.tintColor = elements_color + refreshElement.imageOpacity = 0.5 + + return w; +}