Skip to content

Instantly share code, notes, and snippets.

@grantavery
Last active April 6, 2021 06:45
Show Gist options
  • Save grantavery/4a9587d9190ec58989670da73f787c97 to your computer and use it in GitHub Desktop.
Save grantavery/4a9587d9190ec58989670da73f787c97 to your computer and use it in GitHub Desktop.
iOS Scriptable Widget to display weather and calendar information

WeatherCal Widget

Built and ran as an iOS homescreen widget using the Scriptable app.

This widget takes an OpenWeatherMap API key to provide hourly, daily, and sunset/sunrise weather info. In addition, one section displays upcoming events from the Calendar app.

// Variables used by Scriptable.
// These must be at the very top of the file. Do not edit.
// icon-color: green; icon-glyph: magic;
// this Scriptable Widget is coded by Slowlydev (aka r/Sl0wly-edits, r/Slowlydev)
// and adapted by @marco79 and @grantmavery. Originally named "Date & Agenda & Weather"
// https://gist.github.com/marco79cgn/fa9cd9a3423be4500a20a54cb783f4c0
////// CONSTANTS
const DEV_MODE = false //for developer only
const DEV_PREVIEW = "medium" //for developer only (this script is specialy made for a medium sized widget)
// !!NEW USER INPUT REQUIRED!!
const API_KEY = "" // enter your openweathermap.com api key
const FORECAST_HOURS = "3"
const FORECAST_DAYS = "3"
const UNITS = "imperial" //metric for celsius and imperial for Fahrenheit
const CALENDAR_URL = "calshow://" //Apple Calendar App, if your favorite app does have a URL scheme feel free to change it
const WEATHER_URL = "weatherline://" //there is no URL for the Apple Weather App, if your favorite app does feel free to add it
const widgetBackground = new Color("#D6D6D6") //Widget Background
const stackBackground = new Color("#FFFFFF") //Smaller Container Background
const calendarColor = new Color("#EA3323") //Calendar Color
const stackSize = new Size(0, 65) //0 means it's automatic
// !!NEW USER INPUT REQUIRED!!
// store calendar colors using the name as shown in the Calendar app
colors = {
"Cal1": Color.blue(),
"Cal2": Color.purple(),
"Cal3": Color.yellow(),
"Cal4": Color.white(),
"Cal5": Color.green()
}
// set to true if you want to hide all-day events
hideAllDay = false
////// CREATE WIDGET
if (config.runsInWidget || DEV_MODE) {
const date = new Date()
const dateNow = Date.now()
let df_Name = new DateFormatter()
let df_Month = new DateFormatter()
df_Name.dateFormat = "EEEE"
df_Month.dateFormat = "MMMM"
const dayName = df_Name.string(date)
const dayNumber = date.getDate().toString()
const monthName = df_Month.string(date)
// Option 1: uncomment this to use let the script locate you each time (which takes longer and needs more battery)
let loc = await Location.current()
let lat = loc["latitude"]
let lon = loc["longitude"]
// Option 2: hard coded longitude/latitude
// let lat = ""
// let lon = ""
const weatherURL = `https://api.openweathermap.org/data/2.5/onecall?lat=${lat}&lon=${lon}&exclude=current,minutely,alerts&units=${UNITS}&appid=${API_KEY}`
const weatherRequest = new Request(weatherURL)
const weatherData = await weatherRequest.loadJSON()
const hourlyForecasts = weatherData.hourly
let hourlyNextForecasts = []
for (const hourlyForecast of hourlyForecasts) {
if (hourlyNextForecasts.length == FORECAST_HOURS) { break }
let dt = removeDigits(dateNow, 3)
if (hourlyForecast.dt >= dt) {
hourlyNextForecasts.push(hourlyForecast)
}
}
const dailyForecasts = weatherData.daily
let dailyNextForecasts = []
for (const dailyForecast of dailyForecasts) {
if (dailyNextForecasts.length == FORECAST_DAYS) { break }
let dt = removeDigits(dateNow, 3)
if (dailyForecast.dt >= dt) {
dailyNextForecasts.push(dailyForecast)
}
}
// Find future events that aren't all day and aren't canceled
const events = await CalendarEvent.today([])
let futureEvents = []
for (const event of events) {
if (futureEvents.length == 2) { break }
if (hideAllDay) {
if (event.startDate.getTime() >= date.getTime() && !event.isAllDay && !event.title.startsWith("(Canceled)") && (event.calendar.title in colors)) {
futureEvents.push(event)
}
}
else {
if ((event.calendar.title in colors)
&& (event.isAllDay
|| (event.startDate.getTime() >= date.getTime() && !event.title.startsWith("(Canceled)")))) {
futureEvents.push(event)
}
}
}
let widget = new ListWidget()
widget.backgroundColor = widgetBackground
widget.setPadding(5, 5, 5, 5)
//// Top Row (Hourly and Daily Weather)
let topRow = widget.addStack()
topRow.layoutHorizontally()
widget.addSpacer()
// Top Row Hourly Weather
let hourlyWeatherStack = topRow.addStack()
hourlyWeatherStack.layoutHorizontally()
hourlyWeatherStack.centerAlignContent()
hourlyWeatherStack.setPadding(7, 7, 7, 7)
hourlyWeatherStack.backgroundColor = stackBackground
hourlyWeatherStack.cornerRadius = 12
hourlyWeatherStack.size = stackSize
hourlyWeatherStack.url = WEATHER_URL
for (const nextForecast of hourlyNextForecasts) {
const iconURL = "https://openweathermap.org/img/wn/" + nextForecast.weather[0].icon + "@2x.png"
let iconRequest = new Request(iconURL);
let icon = await iconRequest.loadImage();
hourlyWeatherStack.addSpacer()
let hourStack = hourlyWeatherStack.addStack()
hourStack.layoutVertically()
let hourTxt = hourStack.addText(formatHours(nextForecast.dt))
hourTxt.centerAlignText()
hourTxt.font = Font.systemFont(10)
hourTxt.textColor = Color.black()
hourTxt.textOpacity = 0.5
let weatherIcon = hourStack.addImage(icon)
weatherIcon.centerAlignImage()
weatherIcon.size = new Size(25, 25)
let tempTxt = hourStack.addText(" " + Math.round(nextForecast.temp) + "°")
tempTxt.centerAlignText()
tempTxt.font = Font.systemFont(10)
tempTxt.textColor = Color.black()
}
hourlyWeatherStack.addSpacer()
topRow.addSpacer()
// Top Row Daily Weather
let dailyWeatherStack = topRow.addStack()
dailyWeatherStack.layoutHorizontally()
dailyWeatherStack.centerAlignContent()
dailyWeatherStack.setPadding(7, 2, 7, 2)
dailyWeatherStack.backgroundColor = stackBackground
dailyWeatherStack.cornerRadius = 12
dailyWeatherStack.size = stackSize
dailyWeatherStack.url = WEATHER_URL
for (const nextForecast of dailyNextForecasts) {
const iconURL = "https://openweathermap.org/img/wn/" + nextForecast.weather[0].icon + "@2x.png"
let iconRequest = new Request(iconURL);
let icon = await iconRequest.loadImage();
dailyWeatherStack.addSpacer()
let dayStack = dailyWeatherStack.addStack()
dayStack.layoutVertically()
let hourTxt = dayStack.addText(formatDay(nextForecast.dt))
hourTxt.centerAlignText()
hourTxt.font = Font.systemFont(10)
hourTxt.textColor = Color.black()
hourTxt.textOpacity = 0.5
let weatherIcon = dayStack.addImage(icon)
weatherIcon.centerAlignImage()
weatherIcon.size = new Size(25, 25)
let tempTxt = dayStack.addText(
"" + Math.round(nextForecast.temp.max) +
"-" + Math.round(nextForecast.temp.min) + "°")
tempTxt.centerAlignText()
tempTxt.font = Font.systemFont(10)
tempTxt.textColor = Color.black()
}
dailyWeatherStack.addSpacer()
//// Bottom Row (Events and Sunrise/set)
let bottomRow = widget.addStack()
bottomRow.layoutHorizontally()
// Bottom Row Events
let eventStack = bottomRow.addStack()
eventStack.layoutHorizontally()
eventStack.centerAlignContent()
eventStack.setPadding(7, 2, 7, 2)
eventStack.backgroundColor = stackBackground
eventStack.cornerRadius = 12
eventStack.size = stackSize
let eventInfoStack
const font = Font.lightSystemFont(20)
let calendarSymbol = SFSymbol.named("calendar")
calendarSymbol.applyFont(font)
eventStack.addSpacer(8)
let eventIcon = eventStack.addImage(calendarSymbol.image)
eventIcon.imageSize = new Size(20, 20)
eventIcon.resizable = false
eventIcon.centerAlignImage()
eventStack.addSpacer(14)
eventStack.url = CALENDAR_URL
let eventItemsStack = eventStack.addStack()
eventItemsStack.layoutVertically()
if (futureEvents.length != 0) {
for (let i = 0; i < futureEvents.length; i++) {
let futureEvent = futureEvents[i]
const time = formatTime(futureEvent.startDate) + "-" + formatTime(futureEvent.endDate)
const eventColor = new Color("#" + futureEvent.calendar.color.hex)
eventInfoStack = eventItemsStack.addStack()
eventInfoStack.layoutVertically()
let eventTitle = eventItemsStack.addText(futureEvent.title)
eventTitle.font = Font.semiboldSystemFont(12)
eventTitle.textColor = eventColor
eventTitle.lineLimit = 1
let eventTime = eventItemsStack.addText(time)
eventTime.font = Font.semiboldMonospacedSystemFont(10)
eventTime.textColor = Color.black()
eventTime.textOpacity = 0.5
if (i == 0) {
eventItemsStack.addSpacer(3)
}
}
} else {
let nothingText = eventStack.addText("You have no upcoming events!")
nothingText.font = Font.semiboldMonospacedSystemFont(12)
nothingText.textColor = Color.black()
nothingText.textOpacity = 0.5
}
eventStack.addSpacer()
bottomRow.addSpacer()
// Bottom Row Sunrise/set
let sunStack = bottomRow.addStack()
sunStack.layoutHorizontally()
sunStack.centerAlignContent()
sunStack.setPadding(7, 7, 7, 7)
sunStack.url = WEATHER_URL
sunStack.backgroundColor = stackBackground
sunStack.cornerRadius = 12
sunStack.size = stackSize
createSymbolStack(sunStack, dailyNextForecasts[0].sunrise, "sunrise.fill")
createSymbolStack(sunStack, dailyNextForecasts[0].sunset, "sunset.fill")
sunStack.addSpacer()
Script.setWidget(widget)
if (DEV_MODE) {
if (DEV_PREVIEW == "small") { widget.presentSmall() }
if (DEV_PREVIEW == "medium") { widget.presentMedium() }
if (DEV_PREVIEW == "large") { widget.presentLarge() }
}
Script.complete()
}
////// FUNCTIONS
function removeDigits(x, n) { return (x - (x % Math.pow(10, n))) / Math.pow(10, n) }
function formatHours(UNIX_timestamp) {
var date = new Date(UNIX_timestamp * 1000)
var hours = date.getHours()
var ampm = hours >= 12 ? ' PM' : ' AM'
hours = hours % 12
hours = hours ? hours : 12
var strTime = hours.toString() + ampm
return strTime
}
function formatHoursMin(UNIX_timestamp) {
var date = new Date(UNIX_timestamp * 1000)
var hours = date.getHours()
var minutes = "0" + date.getMinutes();
var ampm = hours >= 12 ? ' PM' : ' AM'
hours = hours % 12
hours = hours ? hours : 12
var strTime = hours.toString() + ":" + minutes.substr(-2) + ampm
return strTime
}
function formatDay(UNIX_timestamp) {
var date = new Date(UNIX_timestamp * 1000)
var day = date.getDay()
var dayStr = ""
switch (day) {
case (0):
dayStr = " Sun"
break
case (1):
dayStr = " Mon"
break
case (2):
dayStr = " Tue"
break
case (3):
dayStr = " Wed"
break
case (4):
dayStr = " Thu"
break
case (5):
dayStr = " Fri"
break
case (6):
dayStr = " Sat"
break
}
return dayStr
}
function formatTime(date) {
let df = new DateFormatter()
df.useNoDateStyle()
df.useShortTimeStyle()
return df.string(date)
}
async function getImg(image) {
let fm = FileManager.iCloud()
let dir = fm.documentsDirectory()
let path = fm.joinPath(dir + "/imgs/weather", image)
let download = await fm.downloadFileFromiCloud(path)
let isDownloaded = await fm.isFileDownloaded(path)
if (fm.fileExists(path)) {
return fm.readImage(path)
} else {
console.log("Error: File does not exist.")
}
}
function createSymbolStack(sunStack, UNIX_timestamp, symbolName) {
sunStack.addSpacer()
let sunsetStack = sunStack.addStack()
sunsetStack.layoutVertically()
let sunsetTxt = sunsetStack.addText(formatHoursMin(UNIX_timestamp))
sunsetTxt.centerAlignText()
sunsetTxt.font = Font.systemFont(10)
sunsetTxt.textColor = Color.black()
let sunsetSymbol = SFSymbol.named(symbolName)
let sunsetIcon = sunsetStack.addImage(sunsetSymbol.image)
sunsetIcon.tintColor = Color.black()
sunsetIcon.centerAlignImage()
sunsetIcon.imageSize = new Size(30, 30)
}
@grantavery
Copy link
Author

Hmm, I'm not sure. That might be a better question for the broader community. I'd make a post on https://talk.automators.fm/t/widget-examples/7994 with your current code.

@schl3ck
Copy link

schl3ck commented Jan 22, 2021

@deta362 I don't know if you've already found the answer, but to center align them, you put them in a horizontal stack and add a spacer on both sides. This of course only works in a vertical stack.

The changes to be made would be:
diff --git a/WeatherCal Widget.js b/WeatherCal Widget.js
index 7dd1aef..a73e77d 100644
--- a/WeatherCal Widget.js	
+++ b/WeatherCal Widget.js	
@@ -138,20 +138,29 @@ if (config.runsInWidget || DEV_MODE) {
     let hourStack = hourlyWeatherStack.addStack()
     hourStack.layoutVertically()
 
-    let hourTxt = hourStack.addText(formatHours(nextForecast.dt))
-    hourTxt.centerAlignText()
+    let stack = hourStack.addStack()
+    stack.layoutHorizontally()
+    stack.addSpacer()
+    let hourTxt = stack.addText(formatHours(nextForecast.dt))
     hourTxt.font = Font.systemFont(10)
     hourTxt.textColor = Color.black()
     hourTxt.textOpacity = 0.5
+    stack.addSpacer()
 
-    let weatherIcon = hourStack.addImage(icon)
-    weatherIcon.centerAlignImage()
+    stack = hourStack.addStack()
+    stack.layoutHorizontally()
+    stack.addSpacer()
+    let weatherIcon = stack.addImage(icon)
     weatherIcon.size = new Size(25, 25)
+    stack.addSpacer()
 
-    let tempTxt = hourStack.addText("  " + Math.round(nextForecast.temp) + "°")
-    tempTxt.centerAlignText()
+    stack = hourStack.addStack()
+    stack.layoutHorizontally()
+    stack.addSpacer()
+    let tempTxt = stack.addText(Math.round(nextForecast.temp) + "°")
     tempTxt.font = Font.systemFont(10)
     tempTxt.textColor = Color.black()
+    stack.addSpacer()
   }
 
   hourlyWeatherStack.addSpacer()
@@ -180,22 +189,31 @@ if (config.runsInWidget || DEV_MODE) {
     let dayStack = dailyWeatherStack.addStack()
     dayStack.layoutVertically()
 
-    let hourTxt = dayStack.addText(formatDay(nextForecast.dt))
-    hourTxt.centerAlignText()
+    let stack = dayStack.addStack()
+    stack.layoutHorizontally()
+    stack.addSpacer()
+    let hourTxt = stack.addText(formatDay(nextForecast.dt))
     hourTxt.font = Font.systemFont(10)
     hourTxt.textColor = Color.black()
     hourTxt.textOpacity = 0.5
+    stack.addSpacer()
 
-    let weatherIcon = dayStack.addImage(icon)
-    weatherIcon.centerAlignImage()
+    stack = dayStack.addStack()
+    stack.layoutHorizontally()
+    stack.addSpacer()
+    let weatherIcon = stack.addImage(icon)
     weatherIcon.size = new Size(25, 25)
+    stack.addSpacer()
 
-    let tempTxt = dayStack.addText(
+    stack = dayStack.addStack()
+    stack.layoutHorizontally()
+    stack.addSpacer()
+    let tempTxt = stack.addText(
       "" + Math.round(nextForecast.temp.max) +
       "-" + Math.round(nextForecast.temp.min) + "°")
-    tempTxt.centerAlignText()
     tempTxt.font = Font.systemFont(10)
     tempTxt.textColor = Color.black()
+    stack.addSpacer()
   }
 
   dailyWeatherStack.addSpacer()
@@ -385,14 +403,21 @@ function createSymbolStack(sunStack, UNIX_timestamp, symbolName) {
   
   let sunsetStack = sunStack.addStack()
   sunsetStack.layoutVertically()
-  let sunsetTxt = sunsetStack.addText(formatHoursMin(UNIX_timestamp))
-  sunsetTxt.centerAlignText()
+
+  let stack = sunsetStack.addStack()
+  stack.layoutHorizontally()
+  stack.addSpacer()
+  let sunsetTxt = stack.addText(formatHoursMin(UNIX_timestamp))
   sunsetTxt.font = Font.systemFont(10)
   sunsetTxt.textColor = Color.black()
+  stack.addSpacer()
 
+  stack = sunsetStack.addStack()
+  stack.layoutHorizontally()
+  stack.addSpacer()
   let sunsetSymbol = SFSymbol.named(symbolName)
-  let sunsetIcon = sunsetStack.addImage(sunsetSymbol.image)
+  let sunsetIcon = stack.addImage(sunsetSymbol.image)
   sunsetIcon.tintColor = Color.black()
-  sunsetIcon.centerAlignImage()
   sunsetIcon.imageSize = new Size(30, 30)
+  stack.addSpacer()

The only problem with that is that some text is now to large and gets cut off with an ellipsis...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment