Widget Examples

since that line is inside function task, you’ll need to add the keyword async for the function definition.

async function tasks(column, alignment)

Once fixed are you able to share the entire code. I am a javascript newbie and aren’t sure whether to place and reference this code in the original code. Thanks in advance.

This is my widget, to show LoL S10 matches in recent two days:


3 Likes

I got daily reminders working with @mzeryck’s modular widget. This is a very basic implementation based on my needs, that will just show any reminders with a due time on the current day and if they’re overdue they’ll show up in red. It doesn’t show all day reminders, and if there are no reminders due today it won’t show anything. I did my best to make it fit in with the modular nature of the original widget but my JavaScript knowledge is very low, so it’s totally conceivable that it won’t work with every setup. Most of this code isn’t mine I just added in the bits that relate to reminders:

/*
 * SETUP
 * Use this section to set up the widget.
 * ======================================
 */

// Get a free API key here: openweathermap.org/appid
const apiKey = ""

// Set to true for fixed location, false to update location as you move around
const lockLocation = true

// Set to imperial for Fahrenheit, or metric for Celsius
const units = "imperial"

// The size of the widget preview in the app.
const widgetPreview = "large"

// Set to true for an image background, false for no image.
const imageBackground = true

// Set to true and run the script once to update the image manually.
const forceImageUpdate = false

// Set the two-letter locale code for the date and weather formatting.
const locale = "en"

// You can change the language or wording of any other text in the widget.
const localizedText = {
  
  // The text shown if you add a greeting item to the layout.
  nightGreeting: "Good night."
  ,morningGreeting: "Good morning."
  ,afternoonGreeting: "Good afternoon."
  ,eveningGreeting: "Good evening."
  
  // The text shown if you add a future weather item to the layout, or tomorrow's events.
  ,nextHourLabel: "Next hour"
  ,tomorrowLabel: "Tomorrow"

  // The text shown in an events item when no events remain.
  // Change to blank "" if you don't want to show a message.
  ,noEventMessage: "Enjoy the rest of your day."
  
//   Reminders header text

,remindersHeader: "Reminders:"
     
}


/*
 * LAYOUT
 * Decide what elements to show on the widget.
 * ===========================================
 */

// Set the width of the column, or set to 0 for an automatic width.

// You can add items to the column: 
// date, greeting, events, current, future, text("Your text here"), tasks
// You can also add a left, center, or right to the list. Everything after it will be aligned that way.

// Make sure to always put a comma after each item.

const columns = [{
  
  // Settings for the left column.
  width: 0,
  items: [
    
    left,
    date,
    events,
    tasks,
    
end]}, {

  // Settings for the right column.
  width: 100,
  items: [
    
    left,
    current,
    space,
    future,
  
end]}]

/*
 * FORMATTING
 * Choose how each element is displayed.
 * =====================================
 */  

// EVENTS
// ======

// How many events to show.
const numberOfEvents = 3

// Show all-day events.
const showAllDay = true

// Show tomorrow's events.
const showTomorrow = true

// Can be blank "" or set to "duration" or "time" to display how long an event is.
const showEventLength = "duration"

// REMINDERS
// =========

// How many reminders to show.
const numberOfTasks = 3

// WEATHER
// =======

// Show today's high and low temperatures.
const showHighLow = true

// Set the hour (in 24-hour time) to switch to tomorrow's weather. Set to 24 to never show it.
const tomorrowShownAtHour = 20

// DATE
// ====

// If set to true, date will become smaller when events are displayed.
const dynamicDateSize = true

// If the date is not dynamic, should it be large or small?
const staticDateSize = "large"

// Determine the date format for each element. See docs.scriptable.app/dateformatter
const smallDateFormat = "EEEE, MMMM d"
const largeDateLineOne = "EEEE,"
const largeDateLineTwo = "MMMM d"

// In this section, set the font, size, and color. Use iosfonts.com to find fonts to use. If you want to use the default iOS font, set the font name to one of the following: ultralight, light, regular, medium, semibold, bold, heavy, black, or italic.
const textFormat = {
  
  // Set the default font and color.
  defaultText: { size: 14, color: "ffffff", font: "regular" },
  
  // Any blank values will use the default.
  smallDate:   { size: 17, color: "", font: "semibold" },
  largeDate1:  { size: 30, color: "", font: "light" },
  largeDate2:  { size: 30, color: "", font: "light" },
  
  greeting:    { size: 30, color: "", font: "semibold" },
  eventTitle:  { size: 14, color: "", font: "semibold" },
  eventTime:   { size: 14, color: "ffffffcc", font: "" },
  
  overdueTask:   { size: 14, color: "FF0000", font: "semibold" },
  
  largeTemp:   { size: 34, color: "", font: "light" },
  smallTemp:   { size: 14, color: "", font: "" },
  tinyTemp:    { size: 12, color: "", font: "" },
  
  customText:  { size: 14, color: "", font: "" } 
}

/*
 * WIDGET CODE
 * Be more careful editing this section. 
 * =====================================
 */

// Set up the date and event information.
const currentDate = new Date()
const todayEvents = await CalendarEvent.today([])
const tomorrowEvents = await CalendarEvent.tomorrow([])
const futureEvents = enumerateEvents()
const eventsAreVisible = (futureEvents.length > 0) && (numberOfEvents > 0)

// Get incomplete reminders for today
const allTasks = await Reminder.incompleteDueToday()

// Set up the file manager.
const files = FileManager.local()

// Set up the location logic.
const locationPath = files.joinPath(files.documentsDirectory(), "weather-cal-location")
var latitude, longitude

// If we're locking our location and it's saved already, read from the file.
if (lockLocation && files.fileExists(locationPath)) {
  const locationStr = files.readString(locationPath).split(",")
  latitude = locationStr[0]
  longitude = locationStr[1]

// Otherwise, get the location from the system.
} else {
  const location = await Location.current()
  latitude = location.latitude
  longitude = location.longitude
  files.writeString(locationPath, latitude + "," + longitude)
}

// Set up the cache.
const cachePath = files.joinPath(files.documentsDirectory(), "weather-cal-cache")
const cacheExists = files.fileExists(cachePath)
const cacheDate = cacheExists ? files.modificationDate(cachePath) : 0
var data

// If cache exists and it's been less than 60 seconds since last request, use cached data.
if (cacheExists && (currentDate.getTime() - cacheDate.getTime()) < 60000) {
  const cache = files.readString(cachePath)
  data = JSON.parse(cache)

// Otherwise, use the API to get new weather data.
} else {
  const weatherReq = "https://api.openweathermap.org/data/2.5/onecall?lat=" + latitude + "&lon=" + longitude + "&exclude=minutely,alerts&units=" + units + "&lang=" + locale + "&appid=" + apiKey
  data = await new Request(weatherReq).loadJSON()
  files.writeString(cachePath, JSON.stringify(data))
}

// Store the weather values.
const currentTemp = data.current.temp
const currentCondition = data.current.weather[0].id
const todayHigh = data.daily[0].temp.max
const todayLow = data.daily[0].temp.min

const nextHourTemp = data.hourly[1].temp
const nextHourCondition = data.hourly[1].weather[0].id

const tomorrowHigh = data.daily[1].temp.max
const tomorrowLow = data.daily[1].temp.min
const tomorrowCondition = data.daily[1].weather[0].id

// Set up the sunrise/sunset cache.
const sunCachePath = files.joinPath(files.documentsDirectory(), "weather-cal-sun")
const sunCacheExists = files.fileExists(sunCachePath)
const sunCacheDate = sunCacheExists ? files.modificationDate(sunCachePath) : 0
var sunData

// If cache exists and it was created today, use cached data.
if (sunCacheExists && sameDay(currentDate, sunCacheDate)) {
  const sunCache = files.readString(sunCachePath)
  sunData = JSON.parse(sunCache)

// Otherwise, use the API to get sunrise and sunset times.
} else {
  const sunReq = "https://api.sunrise-sunset.org/json?lat=" + latitude + "&lng=" + longitude + "&formatted=0&date=" + currentDate.getFullYear() + "-" + (currentDate.getMonth()+1) + "-" + currentDate.getDate()
  sunData = await new Request(sunReq).loadJSON()
  files.writeString(sunCachePath, JSON.stringify(sunData))
}

// Store the timing values.
const sunrise = new Date(sunData.results.sunrise).getTime()
const sunset = new Date(sunData.results.sunset).getTime()
const utcTime = currentDate.getTime()

/*
 * COLUMNS AND PADDING
 * ===================
 */

// Set up the widget and the main stack.
let widget = new ListWidget()
widget.setPadding(0, 0, 0, 0)

let mainStack = widget.addStack()
mainStack.layoutHorizontally()
mainStack.setPadding(0, 0, 0, 0)

// Set up alignment
var currentAlignment = left

// Set up our columns.
for (var x = 0; x < columns.length; x++) {

  let column = columns[x]
  let columnStack = mainStack.addStack()
  columnStack.layoutVertically()
  
  // Only add padding on the first or last column.
  columnStack.setPadding(0, x == 0 ? 5 : 0, 0, x == columns.length-1 ? 5 : 0)
  columnStack.size = new Size(column.width,0)
  
  // Add the items to the column.
  for (var i = 0; i < column.items.length; i++) {
    column.items[i](columnStack)
  }
}

/*
 * BACKGROUND DISPLAY
 * ==================
 */

// If it's an image background, display it.
if (imageBackground) {
  
  // Determine if our image exists and when it was saved.
  const path = files.joinPath(files.documentsDirectory(), "weather-cal-image")
  const exists = files.fileExists(path)
  const createdToday = exists ? sameDay(files.modificationDate(path),currentDate) : false
  
  // If it exists and updates aren't being forced, use the cache.
  if (exists && !forceImageUpdate) { 
    widget.backgroundImage = files.readImage(path)
  
  // If it's missing or forced to update...
  } else if (!exists || forceImageUpdate) { 
    
    // ... just use a gray background if we're in the widget.
    if (config.runsInWidget) { 
      widget.backgroundColor = Color.gray() 
    
    // But if we're running in app, prompt the user for the image.
    } else {
      const img = await Photos.fromLibrary()
      widget.backgroundImage = img
      files.writeImage(path, img)
    }
  }
    
// If it's not an image background, show the gradient.
} else {
  let gradient = new LinearGradient()
  let gradientSettings = getGradientSettings()
  
  gradient.colors = gradientSettings.color()
  gradient.locations = gradientSettings.position()
  
  widget.backgroundGradient = gradient
}

Script.setWidget(widget)
if (widgetPreview == "small") { widget.presentSmall() }
else if (widgetPreview == "medium") { widget.presentMedium() }
else if (widgetPreview == "large") { widget.presentLarge() }
Script.complete()

/*
 * IMAGES AND FORMATTING
 * =====================
 */

// Get the gradient settings for each time of day.
function getGradientSettings() {

  let gradient = {
		"dawn": {
			"color": function() { return [new Color("142C52"), new Color("1B416F"), new Color("62668B")] },
			"position": function() { return [0, 0.5, 1] }
		},
	
		"sunrise": {
			"color": function() { return [new Color("274875"), new Color("766f8d"), new Color("f0b35e")] },
			"position": function() { return [0, 0.8, 1.5] }
		},
	
		"midday": {
			"color": function() { return [new Color("3a8cc1"), new Color("90c0df")] },
			"position": function() { return [0, 1] }
		},
	
		"noon": {
			"color": function() { return [new Color("b2d0e1"), new Color("80B5DB"), new Color("3a8cc1")] },
			"position": function() { return [-0.2, 0.2, 1.5] }
		},
	
		"sunset": {
			"color": function() { return [new Color("32327A"), new Color("662E55"), new Color("7C2F43")] },
			"position": function() { return [0.1, 0.9, 1.2] }
		},
	
		"twilight": {
			"color": function() { return [new Color("021033"), new Color("16296b"), new Color("414791")] },
			"position": function() { return [0, 0.5, 1] }
		},
	
		"night": {
			"color": function() { return [new Color("16296b"), new Color("021033"), new Color("021033"), new Color("113245")] },
			"position": function() { return [-0.5, 0.2, 0.5, 1] }
		}
	}

  function closeTo(time,mins) {
    return Math.abs(utcTime - time) < (mins * 60000)
  }

  // Use sunrise or sunset if we're within 30min of it.
	if (closeTo(sunrise,15)) { return gradient.sunrise }
	if (closeTo(sunset,15)) { return gradient.sunset }

	// In the 30min before/after, use dawn/twilight.
	if (closeTo(sunrise,45) && utcTime < sunrise) { return gradient.dawn }
	if (closeTo(sunset,45) && utcTime > sunset) { return gradient.twilight }

    // Otherwise, if it's night, return night.
	if (isNight(currentDate)) { return gradient.night }

	// If it's around noon, the sun is high in the sky.
	if (currentDate.getHours() == 12) { return gradient.noon }

	// Otherwise, return the "typical" theme.
	return gradient.midday
}

// Provide a symbol based on the condition.
function provideSymbol(cond,night) {
  
  // Define our symbol equivalencies.
  let symbols = {
  
    // Thunderstorm
    "2": function() { return "cloud.bolt.rain.fill" },
    
    // Drizzle
    "3": function() { return "cloud.drizzle.fill" },
    
    // Rain
    "5": function() { return (cond == 511) ? "cloud.sleet.fill" : "cloud.rain.fill" },
    
    // Snow
    "6": function() { return (cond >= 611 && cond <= 613) ? "cloud.snow.fill" : "snow" },
    
    // Atmosphere
    "7": function() {
      if (cond == 781) { return "tornado" }
      if (cond == 701 || cond == 741) { return "cloud.fog.fill" }
      return night ? "cloud.fog.fill" : "sun.haze.fill"
    },
    
    // Clear and clouds
    "8": function() {
      if (cond == 800 || cond == 801) { return night ? "moon.stars.fill" : "sun.max.fill" }
      if (cond == 802 || cond == 803) { return night ? "cloud.moon.fill" : "cloud.sun.fill" }
      return "cloud.fill"
    }
  }
  
  // Find out the first digit.
  let conditionDigit = Math.floor(cond / 100)
  
  // Get the symbol.
  return SFSymbol.named(symbols[conditionDigit]()).image
}

// Provide a font based on the input.
function provideFont(fontName, fontSize) {
  const fontGenerator = {
    "ultralight": function() { return Font.ultraLightSystemFont(fontSize) },
    "light": function() { return Font.lightSystemFont(fontSize) },
    "regular": function() { return Font.regularSystemFont(fontSize) },
    "medium": function() { return Font.mediumSystemFont(fontSize) },
    "semibold": function() { return Font.semiboldSystemFont(fontSize) },
    "bold": function() { return Font.boldSystemFont(fontSize) },
    "heavy": function() { return Font.heavySystemFont(fontSize) },
    "black": function() { return Font.blackSystemFont(fontSize) },
    "italic": function() { return Font.italicSystemFont(fontSize) }
  }
  
  const systemFont = fontGenerator[fontName]
  if (systemFont) { return systemFont() }
  return new Font(fontName, fontSize)
}
 
// Format text based on the settings.
function formatText(textItem, format) {
  const textFont = format.font || textFormat.defaultText.font
  const textSize = format.size || textFormat.defaultText.size
  const textColor = format.color || textFormat.defaultText.color
  
  textItem.font = provideFont(textFont, textSize)
  textItem.textColor = new Color(textColor)
}

/*
 * HELPER FUNCTIONS
 * ================
 */

// Find future events that aren't all day and aren't canceled
function enumerateEvents() {

  // Function to determine if an event should be shown.
  function shouldShowEvent(event) {

    // Hack to remove canceled Office 365 events.
    if (event.title.startsWith("Canceled:")) { return false }

    // If it's an all-day event, only show if the setting is active.
    if (event.isAllDay) { return showAllDay }

    // Otherwise, return the event if it's in the future.
    return (event.startDate.getTime() > currentDate.getTime())
  }
  
  // Determine which events to show, and how many.
  let shownEvents = 0
  let returnedEvents = []
  
  for (const event of todayEvents) {
    if (shownEvents == numberOfEvents) { break }
    if (shouldShowEvent(event)) {
      returnedEvents.push(event)
      shownEvents++
    }
  }

  // If there's room and we need to, show tomorrow's events.
  let multipleTomorrowEvents = false
  if (showTomorrow && shownEvents < numberOfEvents) {

    for (const event of tomorrowEvents) {
      if (shownEvents == numberOfEvents) { break }
      if (shouldShowEvent(event)) {
      
        // Add the tomorrow label prior to the first tomorrow event.
        if (!multipleTomorrowEvents) { 
          
          // The tomorrow label is pretending to be an event.
          returnedEvents.push({ title: localizedText.tomorrowLabel.toUpperCase(), isAllDay: true, isLabel: true })
          multipleTomorrowEvents = true
        }
        
        // Show the tomorrow event and increment the counter.
        returnedEvents.push(event)
        shownEvents++
      }
    }
  }
  return returnedEvents
}

// Determines if the provided date is at night.
function isNight(dateInput) {
  const timeValue = dateInput.getTime()
  return (timeValue < sunrise) || (timeValue > sunset)
}

// Determines if two dates occur on the same day
function sameDay(d1, d2) {
  return d1.getFullYear() === d2.getFullYear() &&
    d1.getMonth() === d2.getMonth() &&
    d1.getDate() === d2.getDate()
}

/*
 * DRAWING FUNCTIONS
 * =================
 */

// Draw the vertical line in the tomorrow view.
function drawVerticalLine() {
  
  const w = 2
  const h = 20
  
  let draw = new DrawContext()
  draw.opaque = false
  draw.respectScreenScale = true
  draw.size = new Size(w,h)
  
  let barPath = new Path()
  const barHeight = h
  barPath.addRoundedRect(new Rect(0, 0, w, h), w/2, w/2)
  draw.addPath(barPath)
  draw.setFillColor(new Color("ffffff", 0.5))
  draw.fillPath()
  
  return draw.getImage()
}

// Draw the temp bar.
function drawTempBar() {

  // Set the size of the temp bar.
  const tempBarWidth = 200
  const tempBarHeight = 20
  
  // Calculate the current percentage of the high-low range.
  let percent = (currentTemp - todayLow) / (todayHigh - todayLow)

  // If we're out of bounds, clip it.
  if (percent < 0) {
    percent = 0
  } else if (percent > 1) {
    percent = 1
  }

  // Determine the scaled x-value for the current temp.
  const currPosition = (tempBarWidth - tempBarHeight) * percent

  // Start our draw context.
  let draw = new DrawContext()
  draw.opaque = false
  draw.respectScreenScale = true
  draw.size = new Size(tempBarWidth, tempBarHeight)

  // Make the path for the bar.
  let barPath = new Path()
  const barHeight = tempBarHeight - 10
  barPath.addRoundedRect(new Rect(0, 5, tempBarWidth, barHeight), barHeight / 2, barHeight / 2)
  draw.addPath(barPath)
  draw.setFillColor(new Color("ffffff", 0.5))
  draw.fillPath()

  // Make the path for the current temp indicator.
  let currPath = new Path()
  currPath.addEllipse(new Rect(currPosition, 0, tempBarHeight, tempBarHeight))
  draw.addPath(currPath)
  draw.setFillColor(new Color("ffffff", 1))
  draw.fillPath()

  return draw.getImage()
}

/*
 * ELEMENTS AND ALIGNMENT
 * ======================
 */

// Create an aligned stack to add content to.
function align(column) {
  
  // Add the containing stack to the column.
  let alignmentStack = column.addStack()
  alignmentStack.layoutHorizontally()
  
  // Get the correct stack from the alignment function.
  let returnStack = currentAlignment(alignmentStack)
  returnStack.layoutVertically()
  return returnStack
}

// Create a right-aligned stack.
function alignRight(alignmentStack) {
  alignmentStack.addSpacer()
  let returnStack = alignmentStack.addStack()
  return returnStack
}

// Create a left-aligned stack.
function alignLeft(alignmentStack) {
  let returnStack = alignmentStack.addStack()
  alignmentStack.addSpacer()
  return returnStack
}

// Create a center-aligned stack.
function alignCenter(alignmentStack) {
  alignmentStack.addSpacer()
  let returnStack = alignmentStack.addStack()
  alignmentStack.addSpacer()
  return returnStack
}

// Display the date on the widget.
function date(column) {

  // Set up the date formatter and set its locale.
  let df = new DateFormatter()
  df.locale = locale
  
  // Show small if it's hard coded, or if it's dynamic and events are visible.
  if ((dynamicDateSize && eventsAreVisible) || staticDateSize == "small") {
    let dateStack = align(column)
    dateStack.setPadding(10, 10, 10, 10)

    df.dateFormat = smallDateFormat
    let dateText = dateStack.addText(df.string(currentDate))
    formatText(dateText, textFormat.smallDate)
    
  // Otherwise, show the large date.
  } else {
    let dateOneStack = align(column)
    df.dateFormat = largeDateLineOne
    let dateOne = dateOneStack.addText(df.string(currentDate))
    formatText(dateOne, textFormat.largeDate1)
    dateOneStack.setPadding(10, 10, 0, 10)
    
    let dateTwoStack = align(column)
    df.dateFormat = largeDateLineTwo
    let dateTwo = dateTwoStack.addText(df.string(currentDate))
    formatText(dateTwo, textFormat.largeDate2)
    dateTwoStack.setPadding(0, 10, 10, 10)
  }
}

function greeting(column) {

  // This function makes a greeting based on the time of day.
  function makeGreeting() {
    const hour = currentDate.getHours()
    if (hour    < 5)  { return localizedText.nightGreeting }
    if (hour    < 12) { return localizedText.morningGreeting }
    if (hour-12 < 5)  { return localizedText.afternoonGreeting }
    if (hour-12 < 10) { return localizedText.eveningGreeting }
    return localizedText.nightGreeting
  }
  
  // Set up the greeting.
  let greetingStack = align(column)
  let greeting = greetingStack.addText(makeGreeting())
  formatText(greeting, textFormat.greeting)
  greetingStack.setPadding(10, 10, 10, 10)
}

// Display events on the widget.
function events(column) {

  // If nothing should be displayed, just return.
  if (!eventsAreVisible && !localizedText.noEventMessage.length) { return }
  
  // Set up the event stack.
  let eventStack = column.addStack()
  eventStack.layoutVertically()
  const todaySeconds = Math.floor(currentDate.getTime() / 1000) - 978307200
  eventStack.url = 'calshow:' + todaySeconds
  
  // If there are no events, show the message and return.
  if (!eventsAreVisible) {
    let message = eventStack.addText(localizedText.noEventMessage)
    formatText(message, textFormat.greeting)
    eventStack.setPadding(10, 10, 10, 10)
    return
  }
  
  // If we're not showing the message, don't pad the event stack.
  eventStack.setPadding(0, 0, 0, 0)
  
  var currentStack = eventStack
  
  // Add each event to the stack.
  for (let i = 0; i < futureEvents.length; i++) {
    
    const event = futureEvents[i]
    
    // If it's the tomorrow label, change to the tomorrow stack.
    if (event.isLabel) {
      let tomorrowStack = column.addStack()
      tomorrowStack.layoutVertically()
      const tomorrowSeconds = Math.floor(currentDate.getTime() / 1000) - 978220800
      tomorrowStack.url = 'calshow:' + tomorrowSeconds
      currentStack = tomorrowStack
    }
    
    const titleStack = align(currentStack)
    const title = titleStack.addText(event.title)
    formatText(title, textFormat.eventTitle)
    titleStack.setPadding(i==0 ? 10 : 5, 10, event.isAllDay ? 5 : 0, 10)
  
    // If there are too many events, limit the line height.
    if (futureEvents.length >= 3) { title.lineLimit = 1 }

    // If it's an all-day event, we don't need a time.
    if (event.isAllDay) { continue }
    
    // Format the time information.
    let df = new DateFormatter()
    df.useNoDateStyle()
    df.useShortTimeStyle()
    let timeText = df.string(event.startDate)
    
    // If we show the length as time, add an en dash and the time.
    if (showEventLength == "time") { 
      timeText += "–" + df.string(event.endDate) 
      
    // If we should it as a duration, add the minutes.
    } else if (showEventLength == "duration") {
      const duration = (event.endDate.getTime() - event.startDate.getTime()) / (1000*60)
      timeText += " \u2022 " + Math.round(duration) + "m"
    }

    const timeStack = align(currentStack)
    const time = timeStack.addText(timeText)
    formatText(time, textFormat.eventTime)
    timeStack.setPadding(0, 10, i==futureEvents.length-1 ? 10 : 5, 10)
  }
}

// Display the current weather.
function current(column) {

  // Set up the current weather stack.
  let currentWeatherStack = column.addStack()
  currentWeatherStack.layoutVertically()
  currentWeatherStack.setPadding(0, 0, 0, 0)
  currentWeatherStack.url = "https://weather.com/weather/today/l/" + latitude + "," + longitude

  // Show the current condition symbol.
  let mainConditionStack = align(currentWeatherStack)
  let mainCondition = mainConditionStack.addImage(provideSymbol(currentCondition,isNight(currentDate)))
  mainCondition.imageSize = new Size(22,22)
  mainConditionStack.setPadding(10, 10, 0, 10)

  // Show the current temperature.
  let tempStack = align(currentWeatherStack)
  let temp = tempStack.addText(Math.round(currentTemp) + "°")
  tempStack.setPadding(0, 10, 0, 10)
  formatText(temp, textFormat.largeTemp)
  
  // If we're not showing the high and low, end it here.
  if (!showHighLow) { return }

  // Show the temp bar and high/low values.
  let tempBarStack = align(currentWeatherStack)
  tempBarStack.layoutVertically()
  tempBarStack.setPadding(0, 10, 5, 10)
  
  let tempBar = drawTempBar()
  let tempBarImage = tempBarStack.addImage(tempBar)
  tempBarImage.size = new Size(50,0)
  
  tempBarStack.addSpacer(1)
  
  let highLowStack = tempBarStack.addStack()
  highLowStack.layoutHorizontally()
  
  let mainLow = highLowStack.addText(Math.round(todayLow).toString())
  highLowStack.addSpacer()
  let mainHigh = highLowStack.addText(Math.round(todayHigh).toString())
  
  formatText(mainHigh, textFormat.tinyTemp)
  formatText(mainLow, textFormat.tinyTemp)
  
  tempBarStack.size = new Size(70,30)
}

// Display upcoming weather.
function future(column) {

  // Set up the future weather stack.
  let futureWeatherStack = column.addStack()
  futureWeatherStack.layoutVertically()
  futureWeatherStack.setPadding(0, 0, 0, 0)
  futureWeatherStack.url = "https://weather.com/weather/tenday/l/" + latitude + "," + longitude

  // Determine if we should show the next hour.
  const showNextHour = (currentDate.getHours() < tomorrowShownAtHour)
  
  // Set the label value.
  const subLabelText = showNextHour ? localizedText.nextHourLabel : localizedText.tomorrowLabel
  let subLabelStack = align(futureWeatherStack)
  let subLabel = subLabelStack.addText(subLabelText)
  formatText(subLabel, textFormat.smallTemp)
  subLabelStack.setPadding(0, 10, 2, 10)
  
  // Set up the sub condition stack.
  let subConditionStack = align(futureWeatherStack)
  subConditionStack.layoutHorizontally()
  subConditionStack.centerAlignContent()
  subConditionStack.setPadding(0, 10, 10, 10)
  
  // Determine what condition to show.
  var nightCondition
  if (showNextHour) {
    const addHour = currentDate.getTime() + (60*60*1000)
    const newDate = new Date(addHour)
    nightCondition = isNight(newDate)
  } else {
    nightCondition = false 
  }
  
  let subCondition = subConditionStack.addImage(provideSymbol(showNextHour ? nextHourCondition : tomorrowCondition,nightCondition))
  const subConditionSize = showNextHour ? 14 : 18
  subCondition.imageSize = new Size(subConditionSize, subConditionSize)
  subConditionStack.addSpacer(5)
  
  // The next part of the display changes significantly for next hour vs tomorrow.
  if (showNextHour) {
    let subTemp = subConditionStack.addText(Math.round(nextHourTemp) + "°")
    formatText(subTemp, textFormat.smallTemp)
    
  } else {
    let tomorrowLine = subConditionStack.addImage(drawVerticalLine())
    tomorrowLine.imageSize = new Size(3,28)
    subConditionStack.addSpacer(5)
    let tomorrowStack = subConditionStack.addStack()
    tomorrowStack.layoutVertically()
    
    let tomorrowHighText = tomorrowStack.addText(Math.round(tomorrowHigh) + "")
    tomorrowStack.addSpacer(4)
    let tomorrowLowText = tomorrowStack.addText(Math.round(tomorrowLow) + "")
    
    formatText(tomorrowHighText, textFormat.tinyTemp)
    formatText(tomorrowLowText, textFormat.tinyTemp)
  }
}

// Return a text-creation function.
function text(inputText) {
  
  function displayText(column) {
    let textStack = align(column)
    textStack.setPadding(10, 10, 10, 10)
    
    let textDisplay = textStack.addText(inputText)
    formatText(textDisplay, textFormat.customText)
  }
  return displayText
}

// Reminders

function tasks(column, alignment) {

// Sort reminders in chronological order  
  function sortItems(first, second) {
        return first.dueDate - second.dueDate
}

if (allTasks.length > 0) {
  let headingStack = align(column, alignment)
  const heading = headingStack.addText(localizedText.remindersHeader);
  formatText(heading, textFormat.eventTitle)
    headingStack.setPadding(10, 10, 0, 10)
  
    let taskStack = align(column, alignment)
    allTasks.sort(sortItems).slice(0, numberOfTasks).forEach(({ title, dueDate}) => {  
//  Show overdue reminders in red with no time     
      if (dueDate < currentDate) {
      const task = taskStack.addText(`○ ${title}`);
        formatText(task, textFormat.overdueTask)
    taskStack.addSpacer(10)
    task.lineLimit = 1;
      }
      
//  Otherwise show upcoming reminders normally
      else {
      const task = taskStack.addText(`○ ${title}`);
      formatText(task, textFormat.eventTitle)
    taskStack.setPadding(10, 10, 10, 10)
      task.lineLimit = 1;
      
      
      let df = new DateFormatter()
    df.useNoDateStyle()
    df.useShortTimeStyle()
    let timeText = df.string(dueDate)
 
const due = taskStack.addText(timeText); 
        formatText(due, textFormat.eventTime)
    taskStack.addSpacer(10)
 }
    });
  }
  
}

/*
 * MINI FUNCTIONS
 * ==============
 */

// This function adds a space.
function space(column) { column.addSpacer() }

// Change the current alignment to right.
function right(x) { currentAlignment = alignRight }

// Change the current alignment to left.
function left(x) { currentAlignment = alignLeft }

// Change the current alignment to center.
function center(x) { currentAlignment = alignCenter }

// This function doesn't need to do anything.
function end(x) { return }

Script.complete()

3 Likes

I converted my YouTube channel stats widget to GitHub repo stars, check out the code here: https://github.com/mrmartineau/scriptable-widgets/tree/main/GitHubRepoStats

Let me know what you think.

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: deep-gray; icon-glyph: chalkboard-teacher;

/**
 * WIDGET CONFIGURATION
 */
const API_URL = 'https://api.github.com/repos/'
const LIGHT_BG_COLOUR = '#2a2a2a'
const DARK_BG_COLOUR = '#111111'

const repoData = await fetch()
const widget = await createWidget(repoData)

// Check if the script is running in
// a widget. If not, show a preview of
// the widget to easier debug it.
if (!config.runsInWidget) {
  await widget.presentSmall()
}
// Tell the system to show the widget.
Script.setWidget(widget)
Script.complete()

async function createWidget({ name, stars, url }) {
  const gradientBg = [
    new Color(`${LIGHT_BG_COLOUR}`),
    new Color(`${DARK_BG_COLOUR}`),
  ]
  const gradient = new LinearGradient()
  gradient.locations = [0, 1]
  gradient.colors = gradientBg
  const bg = new Color(DARK_BG_COLOUR)
  const logoReq = await new Request('https://i.imgur.com/MJzROGa.png')
  const logoImg = await logoReq.loadImage()

  const w = new ListWidget()
  w.useDefaultPadding()
  w.backgroundColor = bg
  w.backgroundGradient = gradient
  w.url = url

  const titleFontSize = 12
  const detailFontSize = 36

  const row = w.addStack()
  row.layoutHorizontally()
  row.addSpacer()
  const wimg = row.addImage(logoImg)
  wimg.imageSize = new Size(30, 30)
  w.addSpacer()

  // Show stars count
  const starsCount = w.addText(formatNumber(`${stars}`))
  starsCount.font = Font.mediumRoundedSystemFont(detailFontSize)
  starsCount.textColor = Color.white()

  const repoName = w.addText(name)
  repoName.font = Font.regularSystemFont(titleFontSize)
  repoName.textColor = Color.white()

  return w
}

async function fetch() {
  const url = `${API_URL}${args.widgetParameter}`
  const req = new Request(url)
  const json = await req.loadJSON()
  return {
    name: json.name,
    stars: json.stargazers_count,
    url: json.html_url,
  }
}

function formatNumber(value) {
  var length = (value + '').length,
    index = Math.ceil((length - 3) / 3),
    suffix = ['k', 'm', 'b', 't']

  if (length < 4) return value

  return (
    (value / Math.pow(1000, index)).toFixed(1).replace(/\.0$/, '') +
    suffix[index - 1]
  )
}
1 Like

You are great! :star_struck: :star_struck: :star_struck: :star_struck: :star_struck: :star_struck: :star_struck: :star_struck: :star_struck: :star_struck: :star_struck: :star_struck: :star_struck: :star_struck:

1 Like

this is amazing! is there an easy way to change the background image when the greeting text changes?

Hi there,

I love the widget view on the right below with the transparent background!
Can you tell me how exactly that you have achieved this? Would you mind sending me the exact code so I can use it? :slight_smile:

Thanks and best regards
Claudio

I updated it so support http://meteoalarm.eu .

These are great! Can the launcher app work on a small size widget? Or just medium?

I tried this wit my spotify credentials and get the token {“error”:“invalid_client”,“error_description”:“Invalid client”}

Is it only possible with a payed spotify membership or what could I have done wrong?

I love this! Thank you so much for sharing! Is there a way to change the size of the weather icons?

I updated it to show an icon for current or fixed location.

Anyone know why I often get this in my widget. It’s a reminders widget and only lasts a few minutes (presumably until the next refresh) - otherwise the widget works fine.

You’d need to share your code somewhere. This isn’t typical, and my guess is that you have some sort of logic that doesn’t execute a setWidget() on occasion.

Is there anyone who can help designing a widget that pulls the contents of a twitter list the user has created or follows?

2 Likes

It’s based on an edited version of some code posted earlier in this thread. Code below:

// Reminders due today and tomorrow widget
var MAX_TASKS_SHOWN = 12;
const NOW = new Date();


const TITLE_FONT = Font.boldSystemFont(8);
const BODY_FONT = Font.semiboldRoundedSystemFont(9);

const today = new Date()
const tomorrow = new Date(today)
  tomorrow.setDate(tomorrow.getDate() + 1)
  tomorrow.setHours(0,0,0,0)
const tomorrow2 = new Date(today)
  tomorrow2.setDate(tomorrow2.getDate() + 2)
  tomorrow2.setHours(0,0,0,0)
  
const BACKGROUND_DARK_MODE = "system" 
// options: "yes", "no", "system"

//
// Utils
//

const compareReminderDuedates = (reminderA, reminderB) =>
  reminderA.dueDate - reminderB.dueDate;
const sortRemindersByDuedateAsc = reminders =>
  reminders.sort(compareReminderDuedates);

/** Round date down to 00:00 */
const stripTime = date => new Date(new Date(date).setHours(0, 0, 0, 0));

/** d1 - d2: will be negative if d2 > d1 */
const daysBetween = (d1, d2) => {
  const differenceMs = stripTime(d1).getTime() - stripTime(d2).getTime();
  return Math.floor(differenceMs / 86400000);
};

const getOverdueTasks = async () => {
  const all = await Reminder.allIncomplete();
  return sortRemindersByDuedateAsc(
    all.filter(task => task.dueDate && task.dueDate < today)
  );
};

const getTodayTasks = async () => {
  const all = await Reminder.allIncomplete();
  return sortRemindersByDuedateAsc(
    all.filter(task => today <= task.dueDate && task.dueDate < tomorrow)
  );
};

const getTomorrowTasks = async () => {
  const all = await Reminder.allIncomplete();
  return sortRemindersByDuedateAsc(
    all.filter(task => tomorrow <= task.dueDate && task.dueDate < tomorrow2)
  );
};

// overdue tasks

const getWidget = async () => {
  const overdueTasks = await getOverdueTasks();
  const todayTasks = await getTodayTasks();
  const tomorrowTasks = await getTomorrowTasks();
  
  const widget = new ListWidget();
  widget.backgroundColor = BG_COLOR;
  
  let isDarkMode = 
    BACKGROUND_DARK_MODE=="system" ? 
    await isUsingDarkAppearance() : 
    BACKGROUND_DARK_MODE=="yes"
    
   if (isDarkMode) {
      var BG_COLOR = new Color("#000000");
      var TITLE_COLOR = new Color("#9E9E9E");
      var OVERDUE_COLOR = new Color("#FE4639");
      var TASK_COLOR = new Color("#FFFFFF");
      var NO_OVERDUE_COLOR = new Color("#2FD15D");
      } else {
      var BG_COLOR = Color.white();
      var TITLE_COLOR = new Color("#8C8C8C");
      var OVERDUE_COLOR = new Color("#FD3F32");
      var TASK_COLOR = new Color("#000000");
      var NO_OVERDUE_COLOR = new Color("#13C759");
  }
  
  widget.addSpacer(0);
  
  const dueNum = overdueTasks.length + todayTasks.length
  if (dueNum) {
    var title = widget.addText(`Today   ${dueNum}`);
  }else{
    var title = widget.addText(`Today`);
  }
  title.textColor = TITLE_COLOR;
  title.font = TITLE_FONT;
  



  if (overdueTasks.length) {
    overdueTasks.slice(0, MAX_TASKS_SHOWN).forEach(({ title, dueDate }) => {
      const task = widget.addText(`| ${title}`);
      task.textColor = OVERDUE_COLOR;
      task.font = BODY_FONT;
      task.lineLimit = 1;
    });
  } else {
    
  }
  
  MAX_TASKS_SHOWN = MAX_TASKS_SHOWN - overdueTasks.length 
  
  if (MAX_TASKS_SHOWN > 0) {
    if (todayTasks.length) {
      todayTasks.slice(0, MAX_TASKS_SHOWN).forEach(({ title, dueDate }) => {
        const task = widget.addText(`| ${title}`);
        task.textColor = TASK_COLOR;
        task.font = BODY_FONT;
        task.lineLimit = 1;
      });
    } else {}
  } else {}
  
  if (dueNum) {
    
  } else {
    const noTasks = widget.addText("All done.");
    noTasks.textColor = NO_OVERDUE_COLOR;
    noTasks.font = BODY_FONT;
  }

// tomorrow

//  const tomorrowTasks = Reminder.allDueTomorrow();
  
  widget.addSpacer(5);

  MAX_TASKS_SHOWN = MAX_TASKS_SHOWN - todayTasks.length - 2
  
  if (MAX_TASKS_SHOWN > 0){
    if (tomorrowTasks.length) {
      const title = widget.addText(`Tomorrow   ${tomorrowTasks.length}`);
      title.textColor = TITLE_COLOR;
      title.font = TITLE_FONT;
      tomorrowTasks.slice(0, MAX_TASKS_SHOWN).forEach(({ title, dueDate }) => {
        const task = widget.addText(`| ${title}`);
        task.textColor = TASK_COLOR;
        task.font = BODY_FONT;
        task.lineLimit = 1;
      });
    } else {}
  } else {}
  
  widget.addSpacer();

  return widget;
};

async function isUsingDarkAppearance() {
  // yes there's a Device.isUsingDarkAppearance() method
  // but I find it unreliable
  const wv = new WebView()
  let js ="(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches)"
  let r = await wv.evaluateJavaScript(js)
  return r
}

(async () => {
  const widget = await getWidget();
  if (config.runsInWidget) {
    Script.setWidget(widget);
    Script.complete();
  } else await widget.presentSmall();
})();

I tried messing around a bit, but I’m not very good at using DrawContexts. I don’t see a way to adjust the SFSymbol image size (since image sizes seem to be read-only).

I’m not great at async/promise/resolve currently, but my thought is that you’re having something timeout which means the widget function never completes before that final block of code runs.

I would say the issue isn’t really that setWidget isn’t running, but that getWidget never finishes executing (maybe due to one of those functions at the top not returning anything).

OK thanks. I’m a real newbie at Javascript so if anyone knows how to fix the issue would appreciate any assistance. Usually when it comes up it is only for a few minutes and then the widget works.