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