├── GitHubRepoStats
├── GitHubRepoStars.js
├── README.md
├── config.jpg
└── repoStars.jpg
├── InstapaperUnread
├── InstapaperUnread.js
├── README.md
├── config.jpg
├── large.jpg
├── medium.jpg
└── small.jpg
├── README.md
└── YoutubeChannelStats
├── README.md
├── YouTube-logo.svg
├── YoutubeChannelSubs+views.js
├── YoutubeChannelSubs.js
├── YoutubeRecentStats.js
├── stats-big.jpg
├── subs-small-alt.jpg
├── subs-small.jpg
└── subs-wide.jpg
/GitHubRepoStats/GitHubRepoStars.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: deep-gray; icon-glyph: chalkboard-teacher;
4 |
5 | /**
6 | * WIDGET CONFIGURATION
7 | */
8 | const API_URL = 'https://api.github.com/repos/'
9 | const LIGHT_BG_COLOUR = '#2a2a2a'
10 | const DARK_BG_COLOUR = '#111111'
11 |
12 | const repoData = await fetch()
13 | const widget = await createWidget(repoData)
14 |
15 | // Check if the script is running in
16 | // a widget. If not, show a preview of
17 | // the widget to easier debug it.
18 | if (!config.runsInWidget) {
19 | await widget.presentSmall()
20 | }
21 | // Tell the system to show the widget.
22 | Script.setWidget(widget)
23 | Script.complete()
24 |
25 | async function createWidget({ name, stars, url }) {
26 | const gradientBg = [
27 | new Color(`${LIGHT_BG_COLOUR}`),
28 | new Color(`${DARK_BG_COLOUR}`),
29 | ]
30 | const gradient = new LinearGradient()
31 | gradient.locations = [0, 1]
32 | gradient.colors = gradientBg
33 | const bg = new Color(DARK_BG_COLOUR)
34 | const logoReq = await new Request('https://i.imgur.com/MJzROGa.png')
35 | const logoImg = await logoReq.loadImage()
36 |
37 | const w = new ListWidget()
38 | w.useDefaultPadding()
39 | w.backgroundColor = bg
40 | w.backgroundGradient = gradient
41 | w.url = url
42 |
43 | const titleFontSize = 12
44 | const detailFontSize = 36
45 |
46 | const row = w.addStack()
47 | row.layoutHorizontally()
48 | row.addSpacer()
49 | const wimg = row.addImage(logoImg)
50 | wimg.imageSize = new Size(30, 30)
51 | w.addSpacer()
52 |
53 | // Show stars count
54 | const starsCount = w.addText(formatNumber(`${stars}`))
55 | starsCount.font = Font.mediumRoundedSystemFont(detailFontSize)
56 | starsCount.textColor = Color.white()
57 |
58 | const repoName = w.addText(name)
59 | repoName.font = Font.regularSystemFont(titleFontSize)
60 | repoName.textColor = Color.white()
61 |
62 | return w
63 | }
64 |
65 | async function fetch() {
66 | const url = `${API_URL}${args.widgetParameter}`
67 | const req = new Request(url)
68 | const json = await req.loadJSON()
69 | return {
70 | name: json.name,
71 | stars: json.stargazers_count,
72 | url: json.html_url,
73 | }
74 | }
75 |
76 | function formatNumber(value) {
77 | var length = (value + '').length,
78 | index = Math.ceil((length - 3) / 3),
79 | suffix = ['k', 'm', 'b', 't']
80 |
81 | if (length < 4) return value
82 |
83 | return (
84 | (value / Math.pow(1000, index)).toFixed(1).replace(/\.0$/, '') +
85 | suffix[index - 1]
86 | )
87 | }
88 |
--------------------------------------------------------------------------------
/GitHubRepoStats/README.md:
--------------------------------------------------------------------------------
1 | # GitHub repo stats
2 |
3 | ## Repository star count
4 |
5 |
6 |
7 | ### Configuration
8 |
9 | To pick the repo, you'll need to add the path as a widget parameter. Only the username & repo name are need here otherwise the script will not work. For example `freeCodeCamp/freeCodeCamp` is what you use for this repo: `https://github.com/freeCodeCamp/freeCodeCamp`.
10 |
11 |
12 |
--------------------------------------------------------------------------------
/GitHubRepoStats/config.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrmartineau/scriptable-widgets/60218937728fc8664e885b3518c947646ad9f06a/GitHubRepoStats/config.jpg
--------------------------------------------------------------------------------
/GitHubRepoStats/repoStars.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrmartineau/scriptable-widgets/60218937728fc8664e885b3518c947646ad9f06a/GitHubRepoStats/repoStars.jpg
--------------------------------------------------------------------------------
/InstapaperUnread/InstapaperUnread.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: light-gray; icon-glyph: book;
4 |
5 | /**
6 | * WIDGET CONFIGURATION
7 | */
8 | const LIGHT_BG_COLOUR = '#F1F1F1'
9 | const DARK_BG_COLOUR = '#CCCCCC'
10 | const INSTAPAPER_RSS_FEED_URL = args.widgetParameter
11 |
12 | const data = await fetchData()
13 | const widget = await createWidget(data)
14 |
15 | // Check if the script is running in
16 | // a widget. If not, show a preview of
17 | // the widget to easier debug it.
18 | if (!config.runsInWidget) {
19 | await widget.presentSmall()
20 | }
21 | // Tell the system to show the widget.
22 | Script.setWidget(widget)
23 | Script.complete()
24 |
25 | async function createWidget(data) {
26 | const gradientBg = [
27 | new Color(`${LIGHT_BG_COLOUR}D9`),
28 | new Color(`${DARK_BG_COLOUR}D9`),
29 | ]
30 | const gradient = new LinearGradient()
31 | gradient.locations = [0, 1]
32 | gradient.colors = gradientBg
33 | const bg = new Color(LIGHT_BG_COLOUR)
34 | const logoReq = await new Request('https://i.imgur.com/BKjVm7c.png')
35 | const logoImg = await logoReq.loadImage()
36 |
37 | const w = new ListWidget()
38 | w.useDefaultPadding()
39 | w.backgroundColor = bg
40 | w.backgroundGradient = gradient
41 | if (config.widgetFamily === 'small') {
42 | w.url = 'instapaper://'
43 | }
44 |
45 | const itemFontSize = config.widgetFamily === 'large' ? 15 : 12
46 |
47 | const headerRow = w.addStack()
48 | headerRow.layoutHorizontally()
49 |
50 | const wimg = headerRow.addImage(logoImg)
51 | wimg.imageSize = new Size(18, 18)
52 | headerRow.addSpacer(10)
53 |
54 | const title =
55 | config.widgetFamily === 'small' ? 'Instapaper' : 'Instapaper: Unread'
56 | const headerTitle = headerRow.addText(title)
57 | headerTitle.font = Font.semiboldSystemFont(15)
58 | headerTitle.textColor = Color.white()
59 | headerTitle.textOpacity = 0.9
60 |
61 | w.addSpacer(10)
62 |
63 | const widgetCount = config.widgetFamily === 'large' ? 7 : 3
64 | data.forEach(({ title, link }, index) => {
65 | if (index > widgetCount) {
66 | return
67 | }
68 | const itemTitle = w.addText(title[0])
69 | itemTitle.font = Font.systemFont(itemFontSize)
70 | itemTitle.textColor = Color.white()
71 | itemTitle.textOpacity = 0.9
72 | itemTitle.url = link[0]
73 |
74 | if (index < widgetCount) {
75 | const spacing = config.widgetFamily === 'large' ? 8 : 6
76 | w.addSpacer(spacing)
77 | }
78 | })
79 |
80 | return w
81 | }
82 |
83 | async function fetch(url) {
84 | const req = new Request(url)
85 | const json = await req.loadJSON()
86 | return json
87 | }
88 |
89 | async function fetchData() {
90 | const unreadData = await fetch(
91 | `https://rsstojson.com/v1/api/?rss_url=${INSTAPAPER_RSS_FEED_URL}`
92 | )
93 |
94 | return unreadData.rss.channel[0].item
95 | }
96 |
97 | function addSymbol({
98 | symbol = 'applelogo',
99 | stack,
100 | color = Color.white(),
101 | size = 20,
102 | }) {
103 | const _sym = SFSymbol.named(symbol)
104 | const wImg = stack.addImage(_sym.image)
105 | wImg.tintColor = color
106 | wImg.imageSize = new Size(size, size)
107 | }
108 |
--------------------------------------------------------------------------------
/InstapaperUnread/README.md:
--------------------------------------------------------------------------------
1 | # Instapaper: Unread
2 |
3 | This widget displays your recent unread articles from Instapaper. It uses your public RSS feed to display the unread items, and when you click them, it opens your default browser to read the article **NOT Instapaper** itself. If I figure out how to open that article directly in Instapaper, I will update this script.
4 |
5 | ### Small
6 |
7 |
8 |
9 | ### Medium
10 |
11 |
12 |
13 | ### Large
14 |
15 |
16 |
17 | ## Config
18 |
19 | Your feed url can be found by visiting https://www.instapaper.com/u and finding the link when viewing the page source and search for `rss`.
20 |
21 | ```html
22 |
28 | ```
29 |
30 | Add `https://www.instapaper.com/` to the front of the `href` value which results in something like this:
31 |
32 | ```
33 | https://www.instapaper.com/rss/123456/YzRvSlLTQDV1lz5OjjeEk4Ogl9d
34 | ```
35 |
36 | Paste your RSS feed url into the widget parameter field when editing the widget.
37 |
38 |
39 |
40 | You could also edit line 10 of the widget and hard-code this value..
41 |
--------------------------------------------------------------------------------
/InstapaperUnread/config.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrmartineau/scriptable-widgets/60218937728fc8664e885b3518c947646ad9f06a/InstapaperUnread/config.jpg
--------------------------------------------------------------------------------
/InstapaperUnread/large.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrmartineau/scriptable-widgets/60218937728fc8664e885b3518c947646ad9f06a/InstapaperUnread/large.jpg
--------------------------------------------------------------------------------
/InstapaperUnread/medium.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrmartineau/scriptable-widgets/60218937728fc8664e885b3518c947646ad9f06a/InstapaperUnread/medium.jpg
--------------------------------------------------------------------------------
/InstapaperUnread/small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrmartineau/scriptable-widgets/60218937728fc8664e885b3518c947646ad9f06a/InstapaperUnread/small.jpg
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
26 |
27 | This is a collection of [Scriptable](https://docs.scriptable.app/) widgets that I've created.
28 |
29 | # GitHub repo star count
30 |
31 | [More info](/GitHubRepoStats)
32 |
33 |
34 |
35 |
36 |
37 | # Instapaper: Unread
38 |
39 | Widget displays your recent unread articles from Instapaper.
40 |
41 | [More info](/InstapaperUnread)
42 |
43 |
44 |
45 |
46 |
47 | # Youtube Channel Stats
48 |
49 | Two different widgets, one for showing your channel subscriber count, and another for showing stats about your channel's 3 most recent uploads.
50 |
51 | [More info](/YoutubeChannelStats)
52 |
53 | ## Subscriber count
54 |
55 |
56 |
57 |
58 |
59 | ## Video stats
60 |
61 |
62 |
63 |
64 |
65 | ---
66 |
67 | Scriptable Documentation: https://docs.scriptable.app/
68 |
69 | Scriptable TestFlight Link: https://testflight.apple.com/join/uN1vTqxk
70 |
--------------------------------------------------------------------------------
/YoutubeChannelStats/README.md:
--------------------------------------------------------------------------------
1 | # YoutubeChannelStats
2 |
3 | These widgets display information about a Youtube channel.
4 |
5 | ### Widget configuration
6 |
7 | There are some variables at the top of the file that you will want to change.
8 |
9 | ```js
10 | /**
11 | * WIDGET CONFIGURATION
12 | */
13 | const YOUTUBE_CHANNEL_ID = 'your_channel_id' // required
14 | const YOUTUBE_API_KEY = 'your_API_key' // required
15 | const SHOW_CHANNEL_TITLE = true
16 | const BG_COLOUR = '#ff0000' // Youtube Red
17 | ```
18 |
19 | - `YOUTUBE_CHANNEL_ID` is the ID of the channel you want to display information about. You can find it from the url of the channel, e.g. for this channel: `https://www.youtube.com/channel/UCaeTwbBs3tezU9MUi25z5MQ`, `UCaeTwbBs3tezU9MUi25z5MQ` is the channel ID. If your channel has a name in the url instead, you can find out the channel ID by using the tool [here](https://commentpicker.com/youtube-channel-id.php)
20 | - The `YOUTUBE_API_KEY` value is an API key that **you** need to create in the [Google Developer Console](https://console.developers.google.com), you can find out how to do this from [here](https://developers.google.com/youtube/v3/getting-started)
21 | - Set `SHOW_CHANNEL_TITLE` to `false` hide the channel title
22 | - Set `DARK_BG_COLOUR` to change the background gradient when your phone is in dark mode
23 | - Set `LIGHT_BG_COLOUR` to change the background gradient when your phone is in light mode
24 |
25 | ## Subscribers count
26 |
27 | File: `YoutubeChannelSubs.js`
28 |
29 | This widget show both your channel subscriber count and total video views. Tapping on the widget will navigate to that Youtube channel.
30 |
31 | ### Small
32 |
33 |
34 |
35 | ### Medium
36 |
37 |
38 |
39 | The count display is formatted so that values with be abbreviated, like so:
40 |
41 | - Count: `62`. Display: `62`
42 | - Count: `623`. Display: `623`
43 | - Count: `6230`. Display: `6.2k`
44 | - Count: `62300`. Display: `62.3k`
45 | - Count: `623000`. Display: `623k`
46 | - Count: `6230000`. Display: `6.2m`
47 | - Count: `62300000`. Display: `623m`
48 | - Count: `623000000`. Display: `6.2b`
49 |
50 | Here's an example for a Youtube channel with over 4 million subscribers.
51 |
52 |
53 |
54 | ## Recent video statistics
55 |
56 | File `YoutubeChannelStats.js`
57 |
58 | This widget shows the like and view counts for your 3 most recent uploads to your channel. It works best as the largest of the widget sizes.
59 |
60 |
61 |
--------------------------------------------------------------------------------
/YoutubeChannelStats/YouTube-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
42 |
--------------------------------------------------------------------------------
/YoutubeChannelStats/YoutubeChannelSubs+views.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: red; icon-glyph: video;
4 |
5 | /**
6 | * WIDGET CONFIGURATION
7 | */
8 | const YOUTUBE_CHANNEL_ID = 'your_channel_id'
9 | const YOUTUBE_API_KEY = 'your_API_key'
10 | const SHOW_CHANNEL_TITLE = true
11 | const DARK_BG_COLOUR = '#000000'
12 | const LIGHT_BG_COLOUR = '#b00a0f'
13 |
14 | let items = await fetchStats()
15 | let widget = await createWidget(items)
16 |
17 | // Check if the script is running in
18 | // a widget. If not, show a preview of
19 | // the widget to easier debug it.
20 | if (!config.runsInWidget) {
21 | await widget.presentMedium()
22 | }
23 | // Tell the system to show the widget.
24 | Script.setWidget(widget)
25 | Script.complete()
26 |
27 | async function createWidget(items) {
28 | const isDarkMode = await isUsingDarkAppearance()
29 | const gradientBg = isDarkMode
30 | ? [new Color(`${DARK_BG_COLOUR}40`), new Color(`${DARK_BG_COLOUR}CC`)]
31 | : [new Color(`${LIGHT_BG_COLOUR}40`), new Color(`${LIGHT_BG_COLOUR}CC`)]
32 | const bg = isDarkMode ? new Color(DARK_BG_COLOUR) : new Color(LIGHT_BG_COLOUR)
33 |
34 | let item = items[0]
35 | const imgReq = await new Request(item.snippet.thumbnails.high.url)
36 | const img = await imgReq.loadImage()
37 | const title = item.snippet.title
38 | const statistics = item.statistics
39 | const { viewCount, subscriberCount } = statistics
40 | let gradient = new LinearGradient()
41 | gradient.locations = [0, 1]
42 | gradient.colors = gradientBg
43 | let w = new ListWidget()
44 | w.backgroundImage = img
45 | w.backgroundColor = bg
46 | w.backgroundGradient = gradient
47 | w.url = `https://www.youtube.com/channel/${YOUTUBE_CHANNEL_ID}`
48 |
49 | const titleFontSize = 12
50 | const detailFontSize = 25
51 |
52 | if (SHOW_CHANNEL_TITLE) {
53 | // Show channel name
54 | let titleTxt = w.addText(title)
55 | titleTxt.font = Font.heavySystemFont(16)
56 | titleTxt.textColor = Color.white()
57 | w.addSpacer(8)
58 | } else {
59 | w.addSpacer()
60 | }
61 |
62 | // Show subscriber count
63 | let subscribersText = w.addText(`SUBSCRIBERS`)
64 | subscribersText.font = Font.mediumSystemFont(titleFontSize)
65 | subscribersText.textColor = Color.white()
66 | subscribersText.textOpacity = 0.9
67 | w.addSpacer(2)
68 |
69 | let subscribersCount = w.addText(formatNumber(subscriberCount))
70 | subscribersCount.font = Font.heavySystemFont(detailFontSize)
71 | subscribersCount.textColor = Color.white()
72 | subscribersCount.textOpacity = 0.9
73 | w.addSpacer(8)
74 |
75 | // Show view count
76 | let viewsText = w.addText(`VIEWS`)
77 | viewsText.font = Font.mediumSystemFont(titleFontSize)
78 | viewsText.textColor = Color.white()
79 | viewsText.textOpacity = 0.9
80 | w.addSpacer(2)
81 |
82 | let viewsCount = w.addText(formatNumber(viewCount))
83 | viewsCount.font = Font.heavySystemFont(detailFontSize)
84 | viewsCount.textColor = Color.white()
85 | viewsCount.textOpacity = 0.9
86 |
87 | return w
88 | }
89 |
90 | async function fetchStats() {
91 | const url = `https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${YOUTUBE_CHANNEL_ID}&key=${YOUTUBE_API_KEY}&part=contentDetails&part=contentOwnerDetails&part=topicDetails&part=snippet`
92 | let req = new Request(url)
93 | let json = await req.loadJSON()
94 | return json.items
95 | }
96 |
97 | async function isUsingDarkAppearance() {
98 | const wv = new WebView()
99 | let js =
100 | "(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)"
101 | let r = await wv.evaluateJavaScript(js)
102 | return r
103 | }
104 |
105 | function formatNumber(number) {
106 | return Intl.NumberFormat('en-US', {
107 | notation: 'compact',
108 | compactDisplay: 'short',
109 | }).format(number)
110 | }
111 |
--------------------------------------------------------------------------------
/YoutubeChannelStats/YoutubeChannelSubs.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: red; icon-glyph: video;
4 |
5 | /**
6 | * WIDGET CONFIGURATION
7 | */
8 | const YOUTUBE_CHANNEL_ID = 'your_channel_id'
9 | const YOUTUBE_API_KEY = 'your_API_key'
10 | const SHOW_CHANNEL_TITLE = false
11 | const LIGHT_BG_COLOUR = '#ff0000'
12 | const DARK_BG_COLOUR = '#9E0000'
13 |
14 | const items = await fetch()
15 | const widget = await createWidget(items)
16 |
17 | // Check if the script is running in
18 | // a widget. If not, show a preview of
19 | // the widget to easier debug it.
20 | if (!config.runsInWidget) {
21 | await widget.presentSmall()
22 | }
23 | // Tell the system to show the widget.
24 | Script.setWidget(widget)
25 | Script.complete()
26 |
27 | async function createWidget(items) {
28 | const item = items[0]
29 | const gradientBg = [
30 | new Color(`${LIGHT_BG_COLOUR}D9`),
31 | new Color(`${DARK_BG_COLOUR}D9`),
32 | ]
33 | const gradient = new LinearGradient()
34 | gradient.locations = [0, 1]
35 | gradient.colors = gradientBg
36 | const bg = new Color(LIGHT_BG_COLOUR)
37 | const imgReq = await new Request(item.snippet.thumbnails.high.url)
38 | const img = await imgReq.loadImage()
39 | const logoReq = await new Request('https://i.imgur.com/mRURHE5.png')
40 | const logoImg = await logoReq.loadImage()
41 |
42 | const title = item.snippet.title
43 | const statistics = item.statistics
44 | const { subscriberCount } = statistics
45 |
46 | const w = new ListWidget()
47 | w.useDefaultPadding()
48 | w.backgroundImage = img
49 | w.backgroundColor = bg
50 | w.backgroundGradient = gradient
51 | w.url = `https://www.youtube.com/channel/${YOUTUBE_CHANNEL_ID}`
52 |
53 | const titleFontSize = 12
54 | const detailFontSize = 50
55 |
56 | const row = w.addStack()
57 | row.layoutHorizontally()
58 | row.addSpacer()
59 | const wimg = row.addImage(logoImg)
60 | wimg.imageSize = new Size(30, 25)
61 |
62 | w.addSpacer()
63 |
64 | // Show subscriber count
65 | const subscribersCount = w.addText(formatNumber(subscriberCount))
66 | subscribersCount.font = Font.mediumRoundedSystemFont(detailFontSize)
67 | subscribersCount.textColor = Color.white()
68 |
69 | const subscribersText = w.addText(`SUBSCRIBERS`)
70 | subscribersText.font = Font.regularSystemFont(titleFontSize)
71 | subscribersText.textColor = Color.white()
72 |
73 | if (SHOW_CHANNEL_TITLE) {
74 | // Show channel name
75 | w.addSpacer(2)
76 | const titleTxt = w.addText(title)
77 | titleTxt.font = Font.heavySystemFont(titleFontSize)
78 | titleTxt.textColor = Color.white()
79 | }
80 |
81 | return w
82 | }
83 |
84 | async function fetch() {
85 | const url = `https://www.googleapis.com/youtube/v3/channels?part=statistics&id=${YOUTUBE_CHANNEL_ID}&key=${YOUTUBE_API_KEY}&part=contentDetails&part=contentOwnerDetails&part=topicDetails&part=snippet`
86 | const req = new Request(url)
87 | const json = await req.loadJSON()
88 | return json.items
89 | }
90 |
91 | function formatNumber(value) {
92 | var length = (value + '').length,
93 | index = Math.ceil((length - 3) / 3),
94 | suffix = ['k', 'm', 'b', 't']
95 |
96 | if (length < 4) return value
97 |
98 | return (
99 | (value / Math.pow(1000, index)).toFixed(1).replace(/\.0$/, '') +
100 | suffix[index - 1]
101 | )
102 | }
103 |
--------------------------------------------------------------------------------
/YoutubeChannelStats/YoutubeRecentStats.js:
--------------------------------------------------------------------------------
1 | // Variables used by Scriptable.
2 | // These must be at the very top of the file. Do not edit.
3 | // icon-color: red; icon-glyph: video;
4 |
5 | /**
6 | * WIDGET CONFIGURATION
7 | */
8 | const YOUTUBE_CHANNEL_ID = 'your_channel_id'
9 | const YOUTUBE_API_KEY = 'your_API_key'
10 | const SHOW_CHANNEL_TITLE = false
11 | const LIGHT_BG_COLOUR = '#ff0000'
12 | const DARK_BG_COLOUR = '#9E0000'
13 |
14 | const stats = await fetchRecentVideoStats()
15 | const widget = await createWidget(stats)
16 |
17 | // Check if the script is running in
18 | // a widget. If not, show a preview of
19 | // the widget to easier debug it.
20 | if (!config.runsInWidget) {
21 | await widget.presentLarge()
22 | }
23 | // Tell the system to show the widget.
24 | Script.setWidget(widget)
25 | Script.complete()
26 |
27 | async function createWidget(stats) {
28 | const gradientBg = [
29 | new Color(`${LIGHT_BG_COLOUR}D9`),
30 | new Color(`${DARK_BG_COLOUR}D9`),
31 | ]
32 | const gradient = new LinearGradient()
33 | gradient.locations = [0, 1]
34 | gradient.colors = gradientBg
35 | const bg = new Color(LIGHT_BG_COLOUR)
36 | const imgReq = await new Request(stats.channelImage)
37 | const img = await imgReq.loadImage()
38 | const logoReq = await new Request('https://i.imgur.com/mRURHE5.png')
39 | const logoImg = await logoReq.loadImage()
40 |
41 | const title = stats.channelName
42 | const statistics = stats.channelStats
43 | const { subscriberCount, viewCount } = statistics
44 |
45 | const w = new ListWidget()
46 | w.useDefaultPadding()
47 | w.backgroundImage = img
48 | w.backgroundColor = bg
49 | w.backgroundGradient = gradient
50 |
51 | w.url = `https://www.youtube.com/channel/${YOUTUBE_CHANNEL_ID}`
52 |
53 | const titleFontSize = 15
54 | const detailFontSize = 20
55 | const emojiFontSize = 14
56 |
57 | const row = w.addStack()
58 | row.layoutHorizontally()
59 | row.addSpacer()
60 | const wimg = row.addImage(logoImg)
61 | wimg.imageSize = new Size(30, 25)
62 |
63 | w.addSpacer()
64 |
65 | stats.videos.forEach(({ title, statistics }) => {
66 | const vidRow = w.addStack()
67 | vidRow.layoutHorizontally()
68 |
69 | const vidViewEmoji = vidRow.addText('👀')
70 | vidViewEmoji.font = new Font('AppleColorEmoji', emojiFontSize)
71 | vidViewEmoji.textOpacity = 0.9
72 | vidRow.addSpacer(5)
73 | w.addSpacer(2)
74 |
75 | const vidViewCount = vidRow.addText(formatNumber(statistics.viewCount))
76 | vidViewCount.font = Font.mediumRoundedSystemFont(detailFontSize)
77 | vidViewCount.textColor = Color.white()
78 | vidViewCount.textOpacity = 0.9
79 | vidRow.addSpacer(12)
80 |
81 | const vidLikeEmoji = vidRow.addText('👍')
82 | vidLikeEmoji.font = new Font('AppleColorEmoji', emojiFontSize)
83 | vidLikeEmoji.textOpacity = 0.9
84 | vidRow.addSpacer(5)
85 |
86 | const vidLikeCount = vidRow.addText(formatNumber(statistics.likeCount))
87 | vidLikeCount.font = Font.mediumRoundedSystemFont(detailFontSize)
88 | vidLikeCount.textColor = Color.white()
89 | vidLikeCount.textOpacity = 0.9
90 |
91 | w.addSpacer(2)
92 | const videoTitle = w.addText(title)
93 | videoTitle.font = Font.mediumSystemFont(titleFontSize)
94 | videoTitle.textColor = Color.white()
95 | videoTitle.textOpacity = 0.9
96 |
97 | w.addSpacer(20)
98 | })
99 | // await table.present()
100 |
101 | if (SHOW_CHANNEL_TITLE) {
102 | // Show channel name
103 | w.addSpacer(2)
104 | const titleTxt = w.addText(title)
105 | titleTxt.font = Font.heavySystemFont(titleFontSize)
106 | titleTxt.textColor = Color.white()
107 | }
108 |
109 | return w
110 | }
111 |
112 | async function fetch(url) {
113 | const req = new Request(url)
114 | const json = await req.loadJSON()
115 | return json.items
116 | }
117 |
118 | async function fetchRecentVideoStats() {
119 | const channelStats = await fetch(
120 | `https://www.googleapis.com/youtube/v3/channels?id=${YOUTUBE_CHANNEL_ID}&key=${YOUTUBE_API_KEY}&part=snippet&part=statistics&part=contentDetails`
121 | )
122 | const uploadsPlaylist =
123 | channelStats[0].contentDetails.relatedPlaylists.uploads
124 | const playlistInfo = await fetch(
125 | `https://www.googleapis.com/youtube/v3/playlistItems?playlistId=${uploadsPlaylist}&key=${YOUTUBE_API_KEY}&part=contentDetails&maxResults=5`
126 | )
127 | const videosIds = playlistInfo.map((item) => {
128 | return item.contentDetails.videoId
129 | })
130 | const video1Info = await fetchVideoInfo(videosIds[0])
131 | const video2Info = await fetchVideoInfo(videosIds[1])
132 | const video3Info = await fetchVideoInfo(videosIds[2])
133 |
134 | return {
135 | channelName: channelStats[0].snippet.title,
136 | channelImage: channelStats[0].snippet.thumbnails.high.url,
137 | channelStats: channelStats[0].statistics,
138 | videos: [
139 | {
140 | title: video1Info[0].snippet.title,
141 | statistics: video1Info[0].statistics,
142 | },
143 | {
144 | title: video2Info[0].snippet.title,
145 | statistics: video2Info[0].statistics,
146 | },
147 | {
148 | title: video3Info[0].snippet.title,
149 | statistics: video3Info[0].statistics,
150 | },
151 | ],
152 | }
153 | }
154 |
155 | async function fetchVideoInfo(videoId) {
156 | return fetch(
157 | `https://www.googleapis.com/youtube/v3/videos?key=${YOUTUBE_API_KEY}&id=${videoId}&part=snippet&part=statistics`
158 | )
159 | }
160 |
161 | function formatNumber(value) {
162 | var length = (value + '').length,
163 | index = Math.ceil((length - 3) / 3),
164 | suffix = ['k', 'm', 'b', 't']
165 |
166 | if (length < 4) return value
167 |
168 | return (
169 | (value / Math.pow(1000, index)).toFixed(1).replace(/\.0$/, '') +
170 | suffix[index - 1]
171 | )
172 | }
173 |
--------------------------------------------------------------------------------
/YoutubeChannelStats/stats-big.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrmartineau/scriptable-widgets/60218937728fc8664e885b3518c947646ad9f06a/YoutubeChannelStats/stats-big.jpg
--------------------------------------------------------------------------------
/YoutubeChannelStats/subs-small-alt.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrmartineau/scriptable-widgets/60218937728fc8664e885b3518c947646ad9f06a/YoutubeChannelStats/subs-small-alt.jpg
--------------------------------------------------------------------------------
/YoutubeChannelStats/subs-small.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrmartineau/scriptable-widgets/60218937728fc8664e885b3518c947646ad9f06a/YoutubeChannelStats/subs-small.jpg
--------------------------------------------------------------------------------
/YoutubeChannelStats/subs-wide.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrmartineau/scriptable-widgets/60218937728fc8664e885b3518c947646ad9f06a/YoutubeChannelStats/subs-wide.jpg
--------------------------------------------------------------------------------