Widget Examples

As the widget is redrawn (and the script rerun then) by the system, you could store a value (to a file or the keychain) and use it to change the gradient…

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

2 posts were split to a new topic: Spotify Playlist

Here is my version, @realliyifei

This week’s upcoming events with date info on top. The date abbreviation is local.

image

(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)
}
// ----------------------------------


2 Likes

Similar to my next game widget (using an API): next concert widget (using my calendar)

(Don’t get jealous fellow U2 fans; fake data…)

3 Likes

And continuing the use of my calendar, but adding the new stacks: displaying the magazines that appear in the next week.

1 Like

@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.

9 posts were split to a new topic: Invisible Widget Generator

Updated version of my next match widget, with team logos, now that we have stacks:

3 Likes

AQI (Air Quality Index) widgets using data from fire.airnow.gov and PurpleAir.

An earlier version during the worst of the fires here, and why I created this in the first place:

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()

5 Likes

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.

6 Likes

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.

2 posts were split to a new topic: Calendar & Reminders