Skip to content

Commit 3d82f11

Browse files
authored
Merge pull request #34 from kevinjalbert/add-ios-widget-tool
feat: add iOS Widget as a new tool (viewing/creating/deleting blocks)
2 parents e5d1d07 + a1bb03b commit 3d82f11

7 files changed

+233
-1
lines changed

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ The following tools are available. Each tool has a README file present in its co
2020
- Provides a REST API to perform CRUD operations with blocks and collections
2121
- Useful to connect with webhooks (e.g., IFTTT)
2222
- Opens integrations with Google Assistant, iOS Shortcuts, and Android Tasker
23-
23+
- [iOS Widget](ios_widget/)
24+
- A [Scriptable](https://scriptable.app/) widget that provides basic functionality for viewing/creating/deleting Notion blocks using iOS Shortcuts
25+
- Works well for Notion pages and collections
2426

2527
## History of this Repository
2628
<details>

ios_widget/Notion Block View.js

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// Variables used by Scriptable.
2+
// These must be at the very top of the file. Do not edit.
3+
// icon-color: deep-blue; icon-glyph: book;
4+
5+
const NOTION_TOKEN = "<token>"
6+
7+
const timestamp = new Date().toLocaleTimeString()
8+
9+
// Determine the block URL (from various sources)
10+
const blockUrl = args.shortcutParameter || args.widgetParameter
11+
12+
// Early exit, which triggers when used in the 'refresh' iOS Shortcut
13+
// For some reason, this lets the widgets 'rerun' properly and update on command
14+
if (!blockUrl) {
15+
Script.complete()
16+
return
17+
}
18+
19+
// Determine block type and extract the correct ids from block URL
20+
const collectionViewRegex = /\/(?<collectionId>\w+)\?v=(?<viewId>\w+$)/
21+
const blockRegex = /\/.*?(?<blockId>\w+)$/
22+
const collectionViewMatches = blockUrl.match(collectionViewRegex)
23+
const blockMatches = blockUrl.match(blockRegex)
24+
let blockType, collectionId, viewId, blockId
25+
if (collectionViewMatches) {
26+
collectionId = collectionViewMatches.groups.collectionId
27+
viewId = collectionViewMatches.groups.viewId
28+
blockType = "collectionView"
29+
} else {
30+
blockId = blockMatches.groups.blockId
31+
blockType = "block"
32+
}
33+
34+
let widget = new ListWidget()
35+
if (blockType === "block") {
36+
await applyBlockStyle(widget)
37+
} else {
38+
await applyCollectionStyle(widget)
39+
}
40+
41+
if (config.runsInWidget) {
42+
Script.setWidget(widget)
43+
} else {
44+
widget.presentLarge()
45+
}
46+
47+
Script.complete()
48+
49+
async function applyBlockStyle(widget) {
50+
const blockJson = await fetchBlock(blockId)
51+
52+
addTitle(widget, blockJson["parent"]["title"], `notion://www.notion.so/${blockId}`)
53+
54+
const childrenStack = widget.addStack()
55+
childrenStack.layoutVertically()
56+
blockJson["children"].forEach(childJson => {
57+
if (!("title" in childJson)) { return }
58+
addChildRow(childrenStack, childJson, "title")
59+
});
60+
61+
// Push content to the top
62+
widget.addSpacer(null)
63+
64+
if (!config.runsWithSiri) {
65+
addFooter(widget, `blocks/${blockId}/children`)
66+
}
67+
68+
return widget
69+
}
70+
71+
async function applyCollectionStyle() {
72+
const collectionViewJson = await fetchCollectionView(collectionId, viewId)
73+
74+
addTitle(widget, collectionViewJson["collection"]["collection_title"], `notion://www.notion.so/${collectionId}?v=${viewId}`)
75+
76+
const childrenStack = widget.addStack()
77+
childrenStack.layoutVertically()
78+
collectionViewJson["rows"].forEach(childJson => {
79+
if (!("name" in childJson)) { return }
80+
addChildRow(childrenStack, childJson, "name")
81+
});
82+
83+
// Push content to the top
84+
widget.addSpacer(null)
85+
86+
if (!config.runsWithSiri) {
87+
addFooter(widget, `collections/${collectionId}/${viewId}`)
88+
}
89+
90+
return widget
91+
}
92+
93+
function addChildRow(childrenStack, childJson, textKey) {
94+
if (!childJson[textKey]) { return }
95+
96+
childStack = childrenStack.addStack()
97+
childStack.centerAlignContent()
98+
99+
const deleteSymbol = SFSymbol.named("trash.fill")
100+
const deleteElement = childStack.addImage(deleteSymbol.image)
101+
deleteElement.imageSize = new Size(16, 16)
102+
deleteElement.tintColor = Color.white()
103+
deleteElement.imageOpacity = 0.75
104+
deleteElement.url = `shortcuts://run-shortcut?name=Delete%20Notion%20Block&input=${childJson["id"]}`
105+
106+
childStack.addSpacer(8)
107+
108+
const titleElement = childStack.addText(childJson[textKey])
109+
titleElement.textColor = Color.white()
110+
titleElement.font = Font.mediumSystemFont(16)
111+
titleElement.minimumScaleFactor = 0.75
112+
titleElement.url = `notion://www.notion.so/${childJson["id"]}`
113+
114+
childrenStack.addSpacer(8)
115+
}
116+
117+
function addTitle(widget, title, url) {
118+
const titleStack = widget.addStack()
119+
titleStack.url = url
120+
titleStack.centerAlignContent()
121+
122+
const linkSymbol = SFSymbol.named("arrow.up.right.square.fill")
123+
const linkElement = titleStack.addImage(linkSymbol.image)
124+
linkElement.imageSize = new Size(16, 16)
125+
linkElement.tintColor = Color.white()
126+
127+
titleStack.addSpacer(6)
128+
129+
const titleElement = titleStack.addText(title)
130+
titleElement.textColor = Color.white()
131+
titleElement.font = Font.boldSystemFont(18)
132+
titleElement.minimumScaleFactor = 0.75
133+
widget.addSpacer(6)
134+
}
135+
136+
function addFooter(widget, addUrlPath) {
137+
const footerStack = widget.addStack()
138+
footerStack.bottomAlignContent()
139+
140+
const addSymbol = SFSymbol.named("plus.square.fill")
141+
const addElement = footerStack.addImage(addSymbol.image)
142+
addElement.imageSize = new Size(20, 20)
143+
addElement.tintColor = Color.white()
144+
addElement.url = `shortcuts://run-shortcut?name=Append%20to%20Notion%20Block&input=${addUrlPath}`
145+
146+
footerStack.addSpacer(null)
147+
148+
const refreshStack = footerStack.addStack()
149+
refreshStack.url = `shortcuts://run-shortcut?name=Refresh%20Notion%20Block&input=${blockUrl}`
150+
refreshStack.centerAlignContent()
151+
152+
const refreshSymbol = SFSymbol.named("arrow.clockwise.icloud.fill")
153+
const refreshElement = refreshStack.addImage(refreshSymbol.image)
154+
refreshElement.imageSize = new Size(20, 20)
155+
refreshElement.tintColor = Color.white()
156+
157+
refreshStack.addSpacer(5)
158+
159+
const updatedAtElement = refreshStack.addText(`Last Sync: ${timestamp}`)
160+
updatedAtElement.textColor = Color.white()
161+
updatedAtElement.textOpacity = 0.6
162+
updatedAtElement.font = Font.mediumSystemFont(10)
163+
}
164+
165+
async function fetchBlock(blockId) {
166+
const request = notionServerRequest(`https://notion-server.herokuapp.com/blocks/${blockId}/children`)
167+
return await request.loadJSON()
168+
}
169+
170+
async function fetchCollectionView(collectionId, viewId) {
171+
const request = notionServerRequest(`https://notion-server.herokuapp.com/collections/${collectionId}/${viewId}`)
172+
return await request.loadJSON()
173+
}
174+
175+
function notionServerRequest(url) {
176+
const request = new Request(url)
177+
request.method = 'GET'
178+
request.headers = {
179+
'Notion-Token': NOTION_TOKEN
180+
}
181+
182+
return request
183+
}

ios_widget/README.md

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Scriptable -- Notion Block View
2+
3+
This is a [Scriptable](https://scriptable.app/) widget that provides basic functionality for viewing/creating/deleting Notion blocks using iOS Shortcuts.
4+
5+
![](./images/widget.jpeg)
6+
7+
# Demo
8+
9+
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.
10+
11+
<div align="left">
12+
<a href="https://www.youtube.com/watch?v=atq6u7Le1JE">
13+
<img src="https://img.youtube.com/vi/atq6u7Le1JE/0.jpg" style="width:70%;">
14+
</a>
15+
</div>
16+
17+
## Installation
18+
19+
To use this widget you have to do the following:
20+
21+
1. Acquire your `token_v2` from [Notion's web application](https://www.notion.so/).
22+
23+
2. (Optional) have your own [notion-toolbox server](../server) running (if the security concern of sending your token to my server scares you)
24+
25+
3. Install [Scriptable](https://scriptable.app/) on your iOS device
26+
27+
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.
28+
29+
5. Create a new script (`Notion Block View`) in Scriptable with the contents in this [file](./Notion\ Block\ View.js) and replace the `NOTION_TOKEN` with your `token_v2` value (and maybe the server url if you decided to use your own)
30+
31+
6. Add a Scriptable widget on your homescreen in iOS
32+
33+
6a. Configure the widget's _Script_ to be `Notion Block View`
34+
35+
6b. Configure the widget's _When Interacting_ to be `Run Script`
36+
37+
6c. Configure the widget's _Parameter_ to be a Notion link for a page/collection
38+
39+
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:
40+
41+
7a. [Append to Notion Block (image)](./images/shortcut-append.jpeg)
42+
43+
7b. [Delete Notion Block (image)](./images/shortcut-delete.jpeg)
44+
45+
7c. [Refresh Notion Block (image)](./images/shortcut-refresh.jpeg)
46+
47+
8. Enjoy
318 KB
Loading
288 KB
Loading
88.9 KB
Loading

ios_widget/images/widget.jpeg

138 KB
Loading

0 commit comments

Comments
 (0)