├── 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 |
2 | 3 | # Scriptable widgets 4 | 5 |

6 | 9 | scriptable widgets is released under the MIT license. 13 | 14 | PRs welcome! 18 | 19 | Follow @MrMartineau 23 | 24 |

25 |
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 | 5 | 6 | 10 | 11 | 12 | 17 | 23 | 25 | 28 | 31 | 32 | 38 | 39 | 40 | 41 | 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 --------------------------------------------------------------------------------