So there’s not a way to make something dynamic and constantly changing like the clock widget?
Hi @realliyifei. Still testing how it behaves when the dates change. The problem now is that the widget hasn’t updated the itself since yesterday.
Adding it again resulted to a widget that does not start at all. I’ve had the same issue with many of the preinstalled examples.
Not through Scriptable as the data is “only” refreshed every 5 minutes. I think that is enough for most uses, but for others it might be not.
The latest TestFlight beta build adds support for stacks and links / tap targets in widgets. Here’s a few examples on how to use the new features.
Please note that as of writing this, some of the APIs used in the following example scripts have not been released on the App Store. You can join the beta through TestFlight if you want to try it out now. As always with Scriptable’s betas, the new APIs might change through the beta. That said, I’m pretty happy with where the APIs are right now
Join the TestFlight beta here:
Animals of the Day
This script uses nested stacks to layout awaits o and image and a title horizontally. The script doesn’t actually fetch the animals from anywhere, but uses hard coded data. I used it as a playground while I was developing the new features
let pandaURL = "https://c402277.ssl.cf1.rackcdn.com/photos/7749/images/story_full_width/HI_204718.jpg?1414503137"
let slothURL = "https://c402277.ssl.cf1.rackcdn.com/photos/6518/images/story_full_width/iStock_000011145477Large_mini_%281%29.jpg?1394632882"
let redPandaURL = "https://c402277.ssl.cf1.rackcdn.com/photos/8036/images/story_full_width/WEB_279173.jpg?1418412345"
let pandaImg = await getImage(pandaURL)
let slothImg = await getImage(slothURL)
let redPandaImg = await getImage(redPandaURL)
let g = new LinearGradient()
g.locations = [0, 1]
g.colors = [
new Color("#1B5E20"),
new Color("#2E7D32")
]
let w = new ListWidget()
w.setPadding(10, 10, 10, 10)
w.spacing = 4
w.backgroundGradient = g
let titleStack = w.addStack()
titleStack.cornerRadius = 4
titleStack.setPadding(2, 5, 2, 5)
titleStack.backgroundColor = new Color("#000", 0.2)
let wtitle = titleStack.addText("Animals of the Day")
wtitle.font = Font.semiboldRoundedSystemFont(14)
wtitle.textColor = Color.white()
w.addSpacer(4)
let row = w.addStack()
addAnimal(
pandaImg,
"Panda",
"https://www.worldwildlife.org/species/giant-panda",
row)
row.addSpacer(20)
addAnimal(
slothImg,
"Sloth",
"https://www.worldwildlife.org/species/sloth",
row)
row.addSpacer(20)
addAnimal(
redPandaImg,
"Red Panda",
"https://www.worldwildlife.org/species/red-panda",
row)
w.presentMedium()
function addAnimal(img, name, link, r) {
let stack = r.addStack()
stack.layoutVertically()
stack.url = link
let wimg = stack.addImage(img)
wimg.cornerRadius = 4
stack.addSpacer(4)
let wname = stack.addText(name)
wname.font = Font.semiboldRoundedSystemFont(14)
wname.textColor = Color.white()
stack.addSpacer(4)
}
async function getImage(url) {
let req = new Request(url)
return await req.loadImage()
}
Launcher
This is an example of a widget that launches other apps. The images referenced in the script should be stored in the imgs/launcher
folder in Scriptable’s folder in the Files app.
I used this shortcut to download app icons from the App Store: https://www.icloud.com/shortcuts/57947afa48ed4e07b37733c4a2ed352f
const IMAGE_NAME = "IMAGE_NAME"
const NAME = "NAME"
const URL = "URL"
const actions1 = [
newAction(
"Text Gro",
"whatsapp.png",
"sms://PHONE-NUMBER"
),
newAction(
"Inbox",
"things.PNG",
"things:///add?show-quick-entry=true&use-clipboard=replace-title"
)
]
const actions2 = [
newAction(
"New Mail",
"spark.png",
"readdle-spark://compose"
),
newAction(
"Hack",
"slack.png",
"slack://channel?team=TPK2S68PN&id=CPWLL7J0P"
)
]
let g = new LinearGradient()
g.locations = [0, 1]
g.colors = [
new Color("#37474f"),
new Color("#455a64")
]
let w = new ListWidget()
w.backgroundGradient = g
w.setPadding(0, 10, 0, 10)
w.addSpacer()
addRow(w, actions1)
w.addSpacer()
addRow(w, actions2)
w.addSpacer()
w.presentMedium()
function addRow(w, actions) {
let s = w.addStack()
for (let i = 0; i < actions.length; i++) {
let a = actions[i]
let image = getImage(a[IMAGE_NAME])
let name = a[NAME]
let container = s.addStack()
container.layoutHorizontally()
container.centerAlignContent()
container.url = a[URL]
container.addSpacer()
let wimg = container.addImage(image)
wimg.imageSize = new Size(50, 50)
wimg.cornerRadius = 11
container.addSpacer(8)
let wname = container.addText(name)
wname.font = Font.semiboldRoundedSystemFont(17)
wname.textColor = Color.white()
if (i < actions.length - 1) {
container.addSpacer()
}
container.addSpacer()
}
}
function newAction(name, imageName, url) {
return {
IMAGE_NAME: imageName,
NAME: name,
URL: url
}
}
function getImage(imageName) {
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let filePath = fm.joinPath(dir, "/imgs/launcher/" + imageName)
return fm.readImage(filePath)
}
On TV Now
I tweaked my script that shows what’s currently on TV to use stacks, so I can show the logo of the TV network rather than it’s name in text.
The script is only really useful in Denmark, as it fetches data from a Danish EPG.
let date = new Date()
let y = ""+date.getFullYear()
let m = ""+(date.getMonth() + 1)
let d = ""+date.getDate()
let dateStr = y+"-"+zeroPrefix(m)+"-"+zeroPrefix(d)
let siriArgs = args.siriShortcutArguments
let channelId = siriArgs.channel
let channelIds = []
if (channelId != null) {
channelIds = [channelId]
} else {
channelIds = [
"1", // DR1
"3", // TV2
"2", // DR2
"10155" // DR3
]
}
let channels = channelIds
.map(e => "ch="+e)
.join("&")
let baseURL = "https://tvtid-api.api.tv2.dk/api/tvtid/v1/epg/dayviews"
let url = baseURL+"/"+dateStr+"?"+channels
let r = new Request(url)
let json = await r.loadJSON()
var s = {}
for (channel of json) {
let id = channel["id"]
let prgs = channel["programs"]
let prg = prgs.find(filterProgram)
s[id] = prg
}
// let widget = createWidget(s)
// await widget.presentMedium()
if (config.runsWithSiri) {
let table = prettySchedule(s)
table.present()
if (channelId != null) {
let prg = s[channelId]
let title = prg.title
Speech.speak(
"There's currently \""
+ title + "\" on "
+ channelTitle(channelId) + ".")
} else {
Speech.speak("Here's what's currently on TV.")
}
} else if (config.runsInWidget) {
let widget = createWidget(s)
Script.setWidget(widget)
Script.complete()
} else {
let table = prettySchedule(s)
await table.present()
}
function createWidget(s) {
log(Object.keys(s))
let channelIds = Object.keys(s)
let g = new LinearGradient()
g.locations = [0, 1]
g.colors = [
new Color("#081040"),
new Color("#0a1860")
]
let w = new ListWidget()
w.backgroundGradient = g
w.setPadding(0, 15, 0, 15)
let l = channelIds.length
for (var i = 0; i < l; i++) {
let id = channelIds[i]
let prg = s[id]
if (prg) {
let subtitle = ""
+ formattedTime(prg["start"])
+ " - "
+ formattedTime(prg["stop"])
let title = prg["title"]
let image = channelImage(id)
let titleStack = w.addStack()
titleStack.layoutHorizontally()
titleStack.centerAlignContent()
if (image != null) {
let wimage = titleStack.addImage(image)
wimage.imageSize = new Size(41, 13)
titleStack.addSpacer(5)
}
let wtitle = titleStack.addText(title)
wtitle.font = Font.mediumSystemFont(15)
wtitle.textOpacity = 1
wtitle.textColor = Color.white()
wtitle.lineLimit = 1
w.addSpacer(2)
let wsubtitle = w.addText(subtitle)
wsubtitle.font = Font.regularSystemFont(13)
wsubtitle.textOpacity = 0.7
wsubtitle.textColor = Color.white()
} else {
// Channel isn't showing anything
let title = channelTitle(id)
let wtitle = w.addText(title)
wtitle.textSize = 13
wtitle.textOpacity = 0.7
wtitle.textColor = Color.white()
let wsubtitle = w.addText("😴")
wsubtitle.textSize = 15
}
if (i < l - 1) {
w.addSpacer(10)
}
}
return w
}
function prettySchedule(s) {
let table = new UITable()
let channelIds = Object.keys(s)
let l = channelIds.length
for (var i = 0; i < l; i++) {
let row = new UITableRow()
let id = channelIds[i]
let prg = s[id]
let channelCell = row.addText(channelTitle(id))
let titleCell
let timeCell
if (prg) {
titleCell = row.addText(prg["title"])
timeCell = row.addText(formattedTime(prg["start"]))
} else {
// Channel isn't showing anything
titleCell = row.addText("😴")
timeCell = row.addText("")
}
channelCell.widthWeight = 15
titleCell.widthWeight = 70
timeCell.widthWeight = 15
table.addRow(row)
}
return table
}
function formattedTime(t) {
let d = new Date(t * 1000)
return ""
+ zeroPrefix(d.getHours().toString())
+ ":"
+ zeroPrefix(d.getMinutes().toString())
}
function channelTitle(id) {
if (id == 1) {
return "DR1"
} else if (id == 2) {
return "DR2"
} else if (id == 3) {
return "TV 2"
} else if (id == 10155) {
return "DR3"
} else {
return "UNKNOWN"
}
}
function channelImage(id) {
let imageName = channelImageName(id)
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let filePath = fm.joinPath(dir, "imgs/channels/" + imageName)
return fm.readImage(filePath)
}
function channelImageName(id) {
if (id == 1) {
return "dr1.png"
} else if (id == 2) {
return "dr2.png"
} else if (id == 3) {
return "tv2.png"
} else if (id == 10155) {
return "dr3.png"
} else {
return null
}
}
function filterProgram(prg) {
let time = new Date().getTime() / 1000
let start = prg["start"]
let stop = prg["stop"]
return time >= start && time <= stop
}
function zeroPrefix(str) {
if (str.length == 1) {
return "0"+str
} else {
return str
}
}
Here is my version, @realliyifei
This week’s upcoming events with date info on top. The date abbreviation is local.
(Shortcoming for some is that it only displays current week events only. I have to fix it so you can choose when next week starts to show.)
Edit: Cleaned up comments and variable names.
// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: red; icon-glyph: calendar-alt;
// This widget is based on code by Max Zeryck @mzeryck
// GitHub: https://gist.github.com/mzeryck/4f9255224fe707ee74d86dc6465feea2
// Scriptable Forum post: https://talk.automators.fm/t/widget-examples/7994/83
//
// Alterations and additional commenting made by Erkka
// The widget shows
// - date, weekday and week number on top
// - next event time and title
// - upcoming event titles
// - this should work in all widget sizes
// - note: the widget shows only current week
// TEST MODE SELECTOR
// If true, tapping the script/widget
// shows a pop-up with refreshed widget
// If false, it the calendar launches
const TEST_MODE = true
// ------------------------
// User tweakable constants
// Font sizes
const headerFont = 20
const firstEvent = 14
const listEvents = 10
// Space before and after the 1st event
const firstPadding = 8
// Space between other events
const eventSpacer = 6
// Max number of events shown
const maxEvents = 7
// Date formatting:
// Check Scriptable documentation: "DateFormat"
// Used here:
// E => short weekday, like "mon"
// dd => day 01-31
// MM => month 01-12
// ww => week number
// Note: You can use additional characters
// and spaces... "(yyyy)" => (2020)
// Header date
const headerDateFormat = "dd/MM E/ww"
// Prefix in event listing
const eventPrefix = "dd/ "
// ---------------
// Other constants
// Let's store the current datetime
const date = new Date()
// -----------------------------
// If the widget is running (and test mode is false)
// tapping the widget (or preas "play" here in the Scriptable editor) the Calendar opens...
if (!config.runsInWidget && !TEST_MODE) {
// I'm not sure what is the logic here,
// but it works
const appleDate = new Date('2001/01/01')
const timestamp = (date.getTime() - appleDate.getTime()) / 1000
const callback = new CallbackURL("calshow:"+timestamp)
callback.open()
Script.complete()
// ...otherwise, create the widget.
} else {
// ---------------------------------
// FIRST WE CREATE THE EVENT ARRAY
// Get calendar events into an array
// scope used: thisWeek
const events = await CalendarEvent.thisWeek([])
// Setup an array
let futureEvents = []
// Put all future events in the array
for (const event of events) {
if (event.startDate.getTime() > date.getTime()){
futureEvents.push(event)
}
}
// Count the events in the array
// We'll use this later
let eventsTotal = futureEvents.length
// ARRAY STUFF IS NOW READY
// ----------------------------
// NEXT: Let's build the widget
// -----------------
// THE WIDGET BEGINS
let widget = new ListWidget()
// FIRST: THE HEADER
// (The top row with current date)
// A new date formatter for the hader date.
// Format set in a const at the beginning.
let dfA = new DateFormatter()
dfA.dateFormat = headerDateFormat
// Print the date header: Take formatted date,
// make it an uppercased string
let headerText = widget.addText(dfA.string(date).toUpperCase())
// ...and now we can STYLIZE it.
// Check Scriptable documentation ("Font")
// to find a list of available fonts
// Font size can be tweaked at the beginning
headerText.font = Font.mediumSystemFont(headerFont)
headerText.textColor = Color.red()
// Store today's date so we can compare it later
dfA.dateFormat = "ddMMyyyy"
let todayIs = dfA.string(date)
// HEADER STUFF IS NOW DONE
// -------------------------
// NEXT: THE EVENT LIST
// We'll need another date formatter,
// This will be used in all events
let dfB = new DateFormatter()
// Store 1st event's date so we can compare it later
dfB.dateFormat = "ddMMyyyy"
let firstIs = dfB.string(futureEvents[0].startDate)
dfB.dateFormat = eventPrefix
// CREATE: 1st EVENT
// First we have to check:
// do we even have any events...
if (eventsTotal < 1) {
// If there are no events,
// we add a spacer and call it a day.
// You could print something nice here :)
widget.addSpacer()
// But if there are events...
} else {
// ...let's define how the 1st entry looks.
// It is a special one and uses 2 lines.
// First some padding. Tweak the value
// at the beginning (const firstPadding)
widget.addSpacer(firstPadding)
// Store 1st event's day and time.
// If the first event is today,
// we won't use a date prefix.
let firstDate
if(todayIs === firstIs){
firstDate = ''
} else {
// Event day number
firstDate = dfB.string(futureEvents[0].startDate) + ' '
}
// Event time span values
let firstStarts = formatTime(futureEvents[0].startDate)
let firstEnds = formatTime(futureEvents[0].endDate)
// PRINT 1st line: 1st event's (date and) time
let firstLine = widget.addText(firstDate + firstStarts + '-' + firstEnds)
// Add some space vetween the lines
widget.addSpacer(5)
// PRINT 2nd line: 1st event's title
let secondLine = widget.addText(futureEvents[0].title)
// STYLIZE: 1st/2nd line font & color
// Font size const (firstEvent)
// was defined at the beginning
firstLine.font = Font.lightSystemFont(firstEvent)
firstLine.textColor = Color.white()
secondLine.font = Font.mediumSystemFont(firstEvent)
secondLine.textColor = Color.red()
// If there's more than 3 events, limit the title
// lines to 1, else limit to 2
if(eventsTotal > 3){ secondLine.lineLimit = 1} else {secondLine.lineLimit = 2}
// Add same amount of padding as we
// did before the first entry
widget.addSpacer(firstPadding)
// FIRST EVENT IS NOW DONE
// -----------------------
// NEXT: EVENTS 2-7 etc.
// Again: We have to check if we have more events.
// If there was only 1...
if (eventsTotal < 2) {
// ...if so, we a spacer...
widget.addSpacer()
// ...otherwise we go on!
} else {
// Now we check how many events we're
// in total (we already printed one)
// Setup a variable
var eventsToList
// We defined the max const at the beginning.
// Now compare that to the event array.
// If we have less than max, value is that
// If we have same as max or more, value is max
if (eventsTotal < maxEvents){eventsToList = eventsTotal} else {eventsToList = maxEvents}
// PRINT the remaining events
var i
// We have already peinted 1 event
// so array index i is set as 1
for(i = 1; i < eventsToList; i++){
// Store day number and title
let eventDateprefix = dfB.string(futureEvents[i].startDate)
let eventTitle = futureEvents[i].title
// Print day + title
let listEvent = widget.addText(eventDateprefix + eventTitle)
// STYLIZE: event line font & color
// Font size const (listEvent)
// was set at the beginning
listEvent.font = Font.regularSystemFont(listEvents)
listEvent.textColor = Color.white()
// Limit lines to 1 per event
listEvent.lineLimit = 1
// Add spacer, const was set at the beginning
widget.addSpacer(eventSpacer)
// For-loop ends here!
}
// The "else" after
// if-we-have-more-than-1-event
// ends here:
}
// ---------------------------------
// The "else" after
// if-we-have-more-than-0-events
// ends here:
}
// ---------------------------------
// A spacer to even things out
widget.addSpacer()
// ------------------------
// Finalize widget settings
widget.setPadding(15,15,15,0)
widget.spacing = -3
Script.setWidget(widget)
widget.presentSmall()
Script.complete()
}
// BOOM. DONE.
// ---------------------------
// FUNCTIONS ARE DEFINED BELOW
// ----------------------------------
// Function: formatTime
// TIME FORMATTER
// Is used to format the event times
function formatTime(date) {
let df = new DateFormatter()
df.useNoDateStyle()
df.useShortTimeStyle()
return df.string(date)
}
// ----------------------------------
Similar to my next game widget (using an API): next concert widget (using my calendar)
(Don’t get jealous fellow U2 fans; fake data…)
And continuing the use of my calendar, but adding the new stacks: displaying the magazines that appear in the next week.
@simonbs could you consider closing or deconsolidating this thread? It’s getting hard to follow and track down posts.
I’m trying to find the example you mentioned elsewhere re: stacks with no success.
Hello, i made here a widget that displays the currently playing song on spotify using the Last.fm API.
Just create a Last.fm account, connect it to Spotify, create a developer account, and get an API key.
You’ll have to paste you user name and api key in the code.
I couldnt make the widget update more frecuently, if someone can just let me know.
//Replace "USER" with you last.fm user and "APIKEY" with the api key for your user.
let url = "http://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=USER&api_key=APIKEY&format=json&limit=1"
let req = new Request(url)
async function createWidget(nowPlaying) {
let widget = new ListWidget()
// load image
const coverArt = await loadImage(nowPlaying.recenttracks.track[0].image[3]["#text"])
widget.backgroundImage = coverArt
widget.addSpacer()
// set gradient background
let startColor = new Color("#1c1c1c19")
let endColor = new Color("#1c1c1cb4")
let gradient = new LinearGradient()
gradient.colors = [startColor, endColor]
gradient.locations = [0.0, 1]
widget.backgroundGradient = gradient
widget.backgroundColor = new Color("1c1c1c")
// add title and artist
let title = nowPlaying.recenttracks.track[0].name.toLowerCase()
// capitalize every first character
title = title.replace(/\b\w/g, function(c) {
return c.toUpperCase();
});
let titleTxt = widget.addText(title)
titleTxt.font = Font.boldSystemFont(12)
titleTxt.textColor = Color.white()
titleTxt.leftAlignText()
widget.addSpacer(2)
let artist = nowPlaying.recenttracks.track[0].artist["#text"]
// capitalize every first character
artist = artist.replace(/\b\w/g, function(c) {
return c.toUpperCase();
});
let artistTxt = widget.addText(artist)
artistTxt.font = Font.systemFont(10)
artistTxt.textColor = Color.yellow()
artistTxt.textOpacity = 1
artistTxt.leftAlignText()
widget.setPadding(8, 15, 10, 5)
widget.url = nowPlaying.recenttracks.track[0].url
return widget
}
// helper function to load and parse a restful json api
async function loadNowPlaying(coverArt) {
const req = new Request(url)
const json = await req.loadJSON()
return json
}
// helper function to download an image from a given url
async function loadImage(imgUrl) {
const url = imgUrl !== null ? imgUrl : placeholder;
const req = new Request(url)
const image = await req.loadImage()
return image
}
const nowPlaying = await loadNowPlaying()
const widget = await createWidget(nowPlaying)
Script.setWidget(widget)
Script.complete()
widget.presentSmall()
Can share your code?
To test the stacks feature I built a Spotify Now Playing widget. It uses the more complex Spotify Authorization Code Flow which is needed to access a user‘s playback information. The widget automatically refreshes expired access tokens. I also built a Siri Shortcut to simplify the initial setup.
Gist and setup instructions:
Caveat:
Even though it’s been a lot of work this widget is more a proof of concept. It will lag behind since it‘s not possible to force an update of widgets and only iOS alone will decide when to refresh.
The Unsplash calendar widget don’t work any more with the pictures.
I think something is wrong with the Unsplash API. If I change the query from “nature” to “nature,water”, it works now.