Widget Examples

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 :blush:

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
  }
}
9 Likes